From ba1281bc2fdc1d5dc60f6b08e054828f432569cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Mon, 13 Nov 2023 00:30:37 +0100 Subject: [PATCH] update for Bevy 0.12 --- Cargo.toml | 13 +- README.md | 13 +- .../a\303\247\303\250t.test" | 0 assets/{example_asset => example_asset.test} | 0 .../subdir/{other_asset => other_asset.test} | 0 build.rs | 14 +- runtime_assets/asset.test | 1 + src/asset_reader.rs | 329 ++++++++++++++++++ src/lib.rs | 299 +++++++--------- src/plugin.rs | 33 -- tests/as_default.rs | 123 +++++++ tests/as_default_with_fallback.rs | 83 +++++ tests/embedded.rs | 117 +++++++ tests/preloaded.rs | 47 --- 14 files changed, 811 insertions(+), 261 deletions(-) rename "assets/a\303\247\303\250t" => "assets/a\303\247\303\250t.test" (100%) rename assets/{example_asset => example_asset.test} (100%) rename assets/subdir/{other_asset => other_asset.test} (100%) create mode 100644 runtime_assets/asset.test create mode 100644 src/asset_reader.rs delete mode 100644 src/plugin.rs create mode 100644 tests/as_default.rs create mode 100644 tests/as_default_with_fallback.rs create mode 100644 tests/embedded.rs delete mode 100644 tests/preloaded.rs diff --git a/Cargo.toml b/Cargo.toml index 065d1ed..2c87c00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_embedded_assets" -version = "0.8.0" +version = "0.9.0" authors = ["François Mockers "] edition = "2021" license = "MIT OR Apache-2.0" @@ -12,11 +12,20 @@ homepage = "https://github.com/vleue/bevy_embedded_assets" documentation = "https://docs.rs/bevy_embedded_assets" categories = ["game-development"] +[features] +default = ["default-source"] +# Support for replacing the default asset source +default-source = ["futures-io", "futures-lite"] + [dependencies.bevy] -version = "0.11" +version = "0.12" default-features = false features = ["bevy_asset"] +[dependencies] +futures-io = { version = "0.3", optional = true } +futures-lite = { version = "2.0", optional = true } + [build-dependencies] cargo-emit = "0.2.1" diff --git a/README.md b/README.md index a110a31..4f7bfd0 100644 --- a/README.md +++ b/README.md @@ -13,19 +13,22 @@ use bevy::prelude::*; use bevy_embedded_assets::EmbeddedAssetPlugin; fn main() { - App::new().add_plugins( - DefaultPlugins - .build() - .add_before::(EmbeddedAssetPlugin), - ); + App::new().add_plugins((EmbeddedAssetPlugin::default(), DefaultPlugins)); } ``` +`EmbeddedAssetPlugin` has three modes: + +* `PluginMode::AutoLoad` will embed the asset folder and make it available through the `embedded://` source +* `PluginMode::ReplaceDefault` will embed the asset folder and make it available through the default source +* `PluginMode::ReplaceAndFallback` will embed the asset folder and make it available through the default source. If a fail is not found at runtime, it fallback to the default source for the current platform + ## Bevy Compatibility |Bevy|bevy_embedded_assets| |---|---| |main|main| +|0.12|0.9| |0.11|0.8| |0.10|0.7| |0.9|0.6| diff --git "a/assets/a\303\247\303\250t" "b/assets/a\303\247\303\250t.test" similarity index 100% rename from "assets/a\303\247\303\250t" rename to "assets/a\303\247\303\250t.test" diff --git a/assets/example_asset b/assets/example_asset.test similarity index 100% rename from assets/example_asset rename to assets/example_asset.test diff --git a/assets/subdir/other_asset b/assets/subdir/other_asset.test similarity index 100% rename from assets/subdir/other_asset rename to assets/subdir/other_asset.test diff --git a/build.rs b/build.rs index 57eaf94..2c3314d 100644 --- a/build.rs +++ b/build.rs @@ -61,8 +61,9 @@ fn main() { let mut file = File::create(dest_path).unwrap(); file.write_all( - "/// Generated function that will add all assets to the [`EmbeddedAssetIo`]. -#[allow(unused_variables, clippy::non_ascii_literal)] pub fn include_all_assets(embedded: &mut EmbeddedAssetIo){\n" + "/// Generated function that will embed all assets. +#[allow(unused_variables, unused_qualifications, clippy::non_ascii_literal)] +fn include_all_assets(mut registry: impl EmbeddedRegistry){\n" .as_ref(), ) .unwrap(); @@ -76,12 +77,12 @@ fn main() { let mut path = path.to_string_lossy().to_string(); if building_for_wasm { // building for wasm. replace paths with forward slash in case we're building from windows - path = path.replace('\\', "/"); + path = path.replace(std::path::MAIN_SEPARATOR, "/"); } cargo_emit::rerun_if_changed!(fullpath.to_string_lossy()); file.write_all( format!( - r#"embedded.add_asset(std::path::Path::new({:?}), include_bytes!({:?})); + r#" registry.insert_included_asset({:?}, include_bytes!({:?})); "#, path, fullpath.to_string_lossy() @@ -98,8 +99,9 @@ fn main() { let mut file = File::create(dest_path).unwrap(); file.write_all( - "/// Generated function that will add all assets to the [`EmbeddedAssetIo`]. - #[allow(unused_variables)] pub fn include_all_assets(embedded: &mut EmbeddedAssetIo){}" + "/// Generated function that will embed all assets. +#[allow(unused_variables, unused_qualifications, clippy::non_ascii_literal)] +fn include_all_assets(registry: impl EmbeddedRegistry){}" .as_ref(), ) .unwrap(); diff --git a/runtime_assets/asset.test b/runtime_assets/asset.test new file mode 100644 index 0000000..48735a1 --- /dev/null +++ b/runtime_assets/asset.test @@ -0,0 +1 @@ +at runtime \ No newline at end of file diff --git a/src/asset_reader.rs b/src/asset_reader.rs new file mode 100644 index 0000000..cef385f --- /dev/null +++ b/src/asset_reader.rs @@ -0,0 +1,329 @@ +use std::{ + io::Read, + path::{Path, PathBuf}, + pin::Pin, + task::Poll, +}; + +use bevy::{ + asset::io::{AssetReader, AssetReaderError, PathStream, Reader}, + utils::HashMap, +}; +use futures_io::AsyncRead; +use futures_lite::Stream; + +use crate::{include_all_assets, EmbeddedRegistry}; + +pub(crate) struct EmbeddedAssetReader { + loaded: HashMap<&'static Path, &'static [u8]>, + fallback: Option>, +} + +impl std::fmt::Debug for EmbeddedAssetReader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EmbeddedAssetReader") + .finish_non_exhaustive() + } +} + +impl Default for EmbeddedAssetReader { + fn default() -> Self { + Self::new() + } +} + +impl EmbeddedRegistry for &mut EmbeddedAssetReader { + fn insert_included_asset(&mut self, name: &'static str, bytes: &'static [u8]) { + self.add_asset(Path::new(name), bytes); + } +} + +impl EmbeddedAssetReader { + /// Create an empty [`EmbeddedAssetReader`]. + #[must_use] + pub fn new() -> Self { + Self { + loaded: HashMap::default(), + fallback: None, + } + } + + /// Create an [`EmbeddedAssetReader`] loaded with all the assets found by the build script. + #[must_use] + pub(crate) fn preloaded() -> Self { + let mut new = Self { + loaded: HashMap::default(), + fallback: None, + }; + include_all_assets(&mut new); + new + } + + /// Create an [`EmbeddedAssetReader`] loaded with all the assets found by the build script. + #[must_use] + pub(crate) fn preloaded_with_default( + mut default: impl FnMut() -> Box + Send + Sync + 'static, + ) -> Self { + let mut new = Self { + loaded: HashMap::default(), + fallback: Some(default()), + }; + include_all_assets(&mut new); + new + } + + /// Add an asset to this [`EmbeddedAssetReader`]. + pub(crate) fn add_asset(&mut self, path: &'static Path, data: &'static [u8]) { + self.loaded.insert(path, data); + } + + /// Get the data from the asset matching the path provided. + /// + /// # Errors + /// + /// This will returns an error if the path is not known. + fn load_path_sync(&self, path: &Path) -> Result { + self.loaded + .get(path) + .map(|b| DataReader(b)) + .ok_or_else(|| AssetReaderError::NotFound(path.to_path_buf())) + } + + fn has_file_sync(&self, path: &Path) -> bool { + self.loaded.contains_key(path) + } + + fn is_directory_sync(&self, path: &Path) -> bool { + let as_folder = path.join(""); + self.loaded + .keys() + .any(|loaded_path| loaded_path.starts_with(&as_folder) && loaded_path != &path) + } + + fn read_directory_sync(&self, path: &Path) -> Result { + if self.is_directory_sync(path) { + let paths: Vec<_> = self + .loaded + .keys() + .filter(|loaded_path| loaded_path.starts_with(path)) + .map(|t| t.to_path_buf()) + .collect(); + Ok(DirReader(paths)) + } else { + Err(AssetReaderError::NotFound(path.to_path_buf())) + } + } +} + +struct DataReader(&'static [u8]); + +impl AsyncRead for DataReader { + fn poll_read( + self: Pin<&mut Self>, + _: &mut std::task::Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let read = self.get_mut().0.read(buf); + Poll::Ready(read) + } +} + +struct DirReader(Vec); + +impl Stream for DirReader { + type Item = PathBuf; + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll> { + let this = self.get_mut(); + Poll::Ready(this.0.pop()) + } +} + +pub(crate) fn get_meta_path(path: &Path) -> PathBuf { + let mut meta_path = path.to_path_buf(); + let mut extension = path + .extension() + .expect("asset paths must have extensions") + .to_os_string(); + extension.push(".meta"); + meta_path.set_extension(extension); + meta_path +} + +impl AssetReader for EmbeddedAssetReader { + fn read<'a>( + &'a self, + path: &'a Path, + ) -> bevy::utils::BoxedFuture<'a, Result>, AssetReaderError>> { + if self.has_file_sync(path) { + Box::pin(async move { + self.load_path_sync(path).map(|reader| { + let boxed: Box = Box::new(reader); + boxed + }) + }) + } else if let Some(fallback) = self.fallback.as_ref() { + fallback.read(path) + } else { + Box::pin(async move { Err(AssetReaderError::NotFound(path.to_path_buf())) }) + } + } + + fn read_meta<'a>( + &'a self, + path: &'a Path, + ) -> bevy::utils::BoxedFuture<'a, Result>, AssetReaderError>> { + let meta_path = get_meta_path(path); + if self.has_file_sync(&meta_path) { + Box::pin(async move { + self.load_path_sync(&meta_path).map(|reader| { + let boxed: Box = Box::new(reader); + boxed + }) + }) + } else if let Some(fallback) = self.fallback.as_ref() { + fallback.read_meta(path) + } else { + Box::pin(async move { Err(AssetReaderError::NotFound(meta_path)) }) + } + } + + fn read_directory<'a>( + &'a self, + path: &'a Path, + ) -> bevy::utils::BoxedFuture<'a, Result, AssetReaderError>> { + Box::pin(async move { + self.read_directory_sync(path).map(|read_dir| { + let boxed: Box = Box::new(read_dir); + boxed + }) + }) + } + + fn is_directory<'a>( + &'a self, + path: &'a Path, + ) -> bevy::utils::BoxedFuture<'a, Result> { + Box::pin(async move { Ok(self.is_directory_sync(path)) }) + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use crate::asset_reader::EmbeddedAssetReader; + + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn load_path() { + let mut embedded = EmbeddedAssetReader::new(); + embedded.add_asset(Path::new("asset.png"), &[1, 2, 3]); + embedded.add_asset(Path::new("other_asset.png"), &[4, 5, 6]); + assert!(embedded.load_path_sync(&Path::new("asset.png")).is_ok()); + assert_eq!( + embedded.load_path_sync(&Path::new("asset.png")).unwrap().0, + [1, 2, 3] + ); + assert_eq!( + embedded + .load_path_sync(&Path::new("other_asset.png")) + .unwrap() + .0, + [4, 5, 6] + ); + assert!(embedded.load_path_sync(&Path::new("asset")).is_err()); + assert!(embedded.load_path_sync(&Path::new("other")).is_err()); + } + + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn is_directory() { + let mut embedded = EmbeddedAssetReader::new(); + embedded.add_asset(Path::new("asset.png"), &[]); + embedded.add_asset(Path::new("directory/asset.png"), &[]); + assert!(!embedded.is_directory_sync(&Path::new("asset.png"))); + assert!(!embedded.is_directory_sync(&Path::new("asset"))); + assert!(embedded.is_directory_sync(&Path::new("directory"))); + assert!(embedded.is_directory_sync(&Path::new("directory/"))); + assert!(!embedded.is_directory_sync(&Path::new("directory/asset"))); + } + + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn read_directory() { + let mut embedded = EmbeddedAssetReader::new(); + embedded.add_asset(Path::new("asset.png"), &[]); + embedded.add_asset(Path::new("directory/asset.png"), &[]); + embedded.add_asset(Path::new("directory/asset2.png"), &[]); + assert!(embedded + .read_directory_sync(&Path::new("asset.png")) + .is_err()); + assert!(embedded + .read_directory_sync(&Path::new("directory")) + .is_ok()); + let mut list = embedded + .read_directory_sync(&Path::new("directory")) + .unwrap() + .0 + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect::>(); + list.sort(); + assert_eq!(list, vec!["directory/asset.png", "directory/asset2.png"]); + } + + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test; + + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn check_preloaded_simple() { + let embedded = EmbeddedAssetReader::preloaded(); + + let path = "example_asset.test"; + + let loaded = embedded.load_path_sync(&Path::new(path)); + assert!(loaded.is_ok()); + let raw_asset = loaded.unwrap(); + assert!(String::from_utf8(raw_asset.0.to_vec()).is_ok()); + assert_eq!(String::from_utf8(raw_asset.0.to_vec()).unwrap(), "hello"); + } + + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn check_preloaded_special_chars() { + let embedded = EmbeddedAssetReader::preloaded(); + + let path = "açèt.test"; + + let loaded = embedded.load_path_sync(&Path::new(path)); + assert!(loaded.is_ok()); + let raw_asset = loaded.unwrap(); + assert!(String::from_utf8(raw_asset.0.to_vec()).is_ok()); + assert_eq!( + String::from_utf8(raw_asset.0.to_vec()).unwrap(), + "with special chars" + ); + } + + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn check_preloaded_subdir() { + let embedded = EmbeddedAssetReader::preloaded(); + + let path = "subdir/other_asset.test"; + + let loaded = embedded.load_path_sync(&Path::new(path)); + assert!(loaded.is_ok()); + let raw_asset = loaded.unwrap(); + assert!(String::from_utf8(raw_asset.0.to_vec()).is_ok()); + assert_eq!( + String::from_utf8(raw_asset.0.to_vec()).unwrap(), + "in subdirectory" + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 8621da4..c1e19a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,194 +12,157 @@ clippy::pedantic )] -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use bevy::{ - asset::{AssetIo, AssetIoError, ChangeWatcher, FileType, Metadata}, - utils::HashMap, + asset::io::embedded::EmbeddedAssetRegistry, + prelude::{App, AssetPlugin, Plugin, Resource}, +}; +#[cfg(feature = "default-source")] +use bevy::{ + asset::io::{AssetSource, AssetSourceId}, + prelude::AssetApp, }; -mod plugin; -pub use plugin::EmbeddedAssetPlugin; +#[cfg(feature = "default-source")] +mod asset_reader; +#[cfg(feature = "default-source")] +use asset_reader::EmbeddedAssetReader; include!(concat!(env!("OUT_DIR"), "/include_all_assets.rs")); -/// An [`HashMap`](bevy::utils::HashMap) associating file paths to their content, that can be used -/// as an [`AssetIo`](bevy::asset::AssetIo) -pub struct EmbeddedAssetIo { - loaded: HashMap<&'static Path, &'static [u8]>, -} - -impl std::fmt::Debug for EmbeddedAssetIo { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("EmbeddedAssetIo").finish_non_exhaustive() - } +/// Bevy plugin to embed all your asset folder. +/// +/// If using the default value of the plugin, or using [`PluginMode::AutoLoad`], assets will be +/// available using the `embedded://` asset source. +/// +/// Order of plugins is not important in this mode, it can be added before or after the +/// `AssetPlugin`. +/// +/// ```rust +/// # use bevy::prelude::*; +/// # use bevy_embedded_assets::EmbeddedAssetPlugin; +/// # #[derive(Asset, TypePath)] +/// # pub struct MyAsset; +/// # fn main() { +/// # let mut app = App::new(); +/// app.add_plugins((EmbeddedAssetPlugin::default(), DefaultPlugins)); +/// # app.init_asset::(); +/// # let asset_server: Mut<'_, AssetServer> = app.world.resource_mut::(); +/// let handle: Handle = asset_server.load("embedded://example_asset.test"); +/// # } +/// ``` +/// +/// If using [`PluginMode::ReplaceDefault`] or [`PluginMode::ReplaceAndFallback`], assets will be +/// available using the default asset source. +/// +/// Order of plugins is important in these modes, it must be added before the `AssetPlugin`. +/// +/// ```rust +/// # use bevy::prelude::*; +/// # use bevy_embedded_assets::{EmbeddedAssetPlugin, PluginMode}; +/// # #[derive(Asset, TypePath)] +/// # pub struct MyAsset; +/// # fn main() { +/// # let mut app = App::new(); +/// app.add_plugins((EmbeddedAssetPlugin { mode: PluginMode::ReplaceDefault }, DefaultPlugins)); +/// # app.init_asset::(); +/// # let asset_server: Mut<'_, AssetServer> = app.world.resource_mut::(); +/// let handle: Handle = asset_server.load("example_asset.test"); +/// # } +/// ``` +/// +/// +#[allow(clippy::module_name_repetitions)] +#[derive(Default, Debug, Clone)] +pub struct EmbeddedAssetPlugin { + /// How this plugin should behave. + pub mode: PluginMode, } -impl Default for EmbeddedAssetIo { - fn default() -> Self { - Self::new() - } -} - -impl EmbeddedAssetIo { - /// Create an empty [`EmbeddedAssetIo`]. - #[must_use] - pub fn new() -> Self { - Self { - loaded: HashMap::default(), - } - } - - /// Create an [`EmbeddedAssetIo`] loaded with all the assets found by the build script. - #[must_use] - pub fn preloaded() -> Self { - let mut new = Self { - loaded: HashMap::default(), - }; - include_all_assets(&mut new); - new - } - - /// Add an asset to this [`EmbeddedAssetIo`]. - pub fn add_asset(&mut self, path: &'static Path, data: &'static [u8]) { - self.loaded.insert(path, data); - } - - /// Get the data from the asset matching the path provided. +/// How [`EmbeddedAssetPlugin`] should behave. +#[derive(Debug, Clone, Default)] +#[allow(missing_copy_implementations)] +pub enum PluginMode { + /// Embed the assets folder and make the files available through the `embedded://` source. + #[default] + AutoLoad, + /// Replace the default asset source with an embedded source. /// - /// # Errors + /// In this mode, listing files in a directory will work in wasm. + #[cfg(feature = "default-source")] + ReplaceDefault, + /// Replace the default asset source with an embedded source. If a file is not present at build + /// time, fallback to the default source for the current platform. /// - /// This will returns an error if the path is not known. - pub fn load_path_sync(&self, path: &Path) -> Result, AssetIoError> { - self.loaded - .get(path) - .map(|b| b.to_vec()) - .ok_or_else(|| bevy::asset::AssetIoError::NotFound(path.to_path_buf())) - } + /// In this mode, listing files in a directory will work in wasm. + #[cfg(feature = "default-source")] + ReplaceAndFallback { + /// The default file path to use (relative to the project root). `"assets"` is the + /// standard value in Bevy. + path: String, + }, } -impl AssetIo for EmbeddedAssetIo { - fn load_path<'a>( - &'a self, - path: &'a Path, - ) -> bevy::utils::BoxedFuture<'a, Result, AssetIoError>> { - Box::pin(async move { self.load_path_sync(path) }) - } +#[derive(Resource, Default)] +struct AllTheEmbedded; - #[allow(clippy::needless_collect)] - fn read_directory( - &self, - path: &Path, - ) -> Result>, AssetIoError> { - if self.get_metadata(path).unwrap().is_dir() { - let paths: Vec<_> = self - .loaded - .keys() - .filter(|loaded_path| loaded_path.starts_with(path)) - .map(|t| t.to_path_buf()) - .collect(); - Ok(Box::new(paths.into_iter())) - } else { - Err(AssetIoError::Io(std::io::ErrorKind::NotFound.into())) - } - } +trait EmbeddedRegistry { + fn insert_included_asset(&mut self, name: &'static str, bytes: &'static [u8]); +} - fn watch_path_for_changes( - &self, - _path: &Path, - _to_reload: Option, - ) -> Result<(), AssetIoError> { - Ok(()) +impl EmbeddedRegistry for &mut EmbeddedAssetRegistry { + fn insert_included_asset(&mut self, name: &str, bytes: &'static [u8]) { + self.insert_asset(PathBuf::new(), std::path::Path::new(name), bytes); } +} - fn watch_for_changes(&self, _configuration: &ChangeWatcher) -> Result<(), AssetIoError> { - Ok(()) +impl Plugin for EmbeddedAssetPlugin { + fn build(&self, app: &mut App) { + match &self.mode { + PluginMode::AutoLoad => { + if app.is_plugin_added::() { + let mut registry = app.world.resource_mut::(); + include_all_assets(registry.as_mut()); + app.init_resource::(); + } + } + #[cfg(feature = "default-source")] + PluginMode::ReplaceDefault => { + if app.is_plugin_added::() { + bevy::log::error!( + "plugin EmbeddedAssetPlugin must be added before plugin AssetPlugin when replacing the default asset source" + ); + } + app.register_asset_source( + AssetSourceId::Default, + AssetSource::build().with_reader(|| Box::new(EmbeddedAssetReader::preloaded())), + ); + } + #[cfg(feature = "default-source")] + PluginMode::ReplaceAndFallback { path } => { + bevy::log::error!( + "plugin EmbeddedAssetPlugin must be added before plugin AssetPlugin when replacing the default asset source" + ); + let path = path.clone(); + app.register_asset_source( + AssetSourceId::Default, + AssetSource::build().with_reader(move || { + Box::new(EmbeddedAssetReader::preloaded_with_default( + AssetSource::get_default_reader(path.clone()), + )) + }), + ); + } + } } - fn get_metadata(&self, path: &Path) -> Result { - let as_folder = path.join(""); - if self - .loaded - .keys() - .any(|loaded_path| loaded_path.starts_with(&as_folder) && loaded_path != &path) + fn finish(&self, app: &mut App) { + if matches!(self.mode, PluginMode::AutoLoad) + && app.world.remove_resource::().is_none() { - Ok(Metadata::new(FileType::Directory)) - } else { - Ok(Metadata::new(FileType::File)) + let mut registry = app.world.resource_mut::(); + include_all_assets(registry.as_mut()); } } } - -#[cfg(test)] -mod tests { - use std::path::Path; - - use bevy::asset::AssetIo; - - use crate::EmbeddedAssetIo; - - #[test] - fn load_path() { - let mut embedded = EmbeddedAssetIo::new(); - embedded.add_asset(Path::new("asset.png"), &[1, 2, 3]); - embedded.add_asset(Path::new("other_asset.png"), &[4, 5, 6]); - - assert!(embedded.load_path_sync(&Path::new("asset.png")).is_ok()); - assert_eq!( - embedded.load_path_sync(&Path::new("asset.png")).unwrap(), - [1, 2, 3] - ); - assert_eq!( - embedded - .load_path_sync(&Path::new("other_asset.png")) - .unwrap(), - [4, 5, 6] - ); - assert!(embedded.load_path_sync(&Path::new("asset")).is_err()); - assert!(embedded.load_path_sync(&Path::new("other")).is_err()); - } - - #[test] - fn is_directory() { - let mut embedded = EmbeddedAssetIo::new(); - embedded.add_asset(Path::new("asset.png"), &[]); - embedded.add_asset(Path::new("directory/asset.png"), &[]); - - assert!(!embedded - .get_metadata(&Path::new("asset.png")) - .unwrap() - .is_dir()); - assert!(!embedded.get_metadata(&Path::new("asset")).unwrap().is_dir()); - assert!(embedded - .get_metadata(&Path::new("directory")) - .unwrap() - .is_dir()); - assert!(embedded - .get_metadata(&Path::new("directory/")) - .unwrap() - .is_dir()); - assert!(!embedded - .get_metadata(&Path::new("directory/asset")) - .unwrap() - .is_dir()); - } - - #[test] - fn read_directory() { - let mut embedded = EmbeddedAssetIo::new(); - embedded.add_asset(Path::new("asset.png"), &[]); - embedded.add_asset(Path::new("directory/asset.png"), &[]); - embedded.add_asset(Path::new("directory/asset2.png"), &[]); - - assert!(embedded.read_directory(&Path::new("asset.png")).is_err()); - assert!(embedded.read_directory(&Path::new("directory")).is_ok()); - let mut list = embedded - .read_directory(&Path::new("directory")) - .unwrap() - .map(|p| p.to_string_lossy().to_string()) - .collect::>(); - list.sort(); - assert_eq!(list, vec!["directory/asset.png", "directory/asset2.png"]); - } -} diff --git a/src/plugin.rs b/src/plugin.rs deleted file mode 100644 index 993b438..0000000 --- a/src/plugin.rs +++ /dev/null @@ -1,33 +0,0 @@ -use bevy::prelude::*; - -/// Bevy plugin to add to your application that will insert a custom [`AssetServer`] embedding -/// your assets instead of the default added by the [`AssetPlugin`](bevy::asset::AssetPlugin). -/// If you are using the [`DefaultPlugins`] group from Bevy, it can be added this way: -/// -/// ```rust -/// # use bevy::prelude::*; -/// # use bevy_embedded_assets::EmbeddedAssetPlugin; -/// # fn main() { -/// App::new().add_plugins( -/// DefaultPlugins -/// .build() -/// .add_before::(EmbeddedAssetPlugin), -/// ); -/// # } -/// ``` -#[allow( - missing_debug_implementations, - missing_copy_implementations, - clippy::module_name_repetitions -)] -#[derive(Default)] -pub struct EmbeddedAssetPlugin; - -impl Plugin for EmbeddedAssetPlugin { - fn build(&self, app: &mut App) { - if app.is_plugin_added::() { - error!("plugin EmbeddedAssetPlugin must be added before plugin AssetPlugin"); - } - app.insert_resource(AssetServer::new(crate::EmbeddedAssetIo::preloaded())); - } -} diff --git a/tests/as_default.rs b/tests/as_default.rs new file mode 100644 index 0000000..7afae97 --- /dev/null +++ b/tests/as_default.rs @@ -0,0 +1,123 @@ +#![cfg(feature = "default-source")] + +use std::fmt::Display; + +use bevy::{prelude::*, utils::thiserror::Error}; +use bevy_embedded_assets::{EmbeddedAssetPlugin, PluginMode}; + +#[derive(Asset, TypePath, Debug)] +pub struct TestAsset { + pub value: String, +} + +#[derive(Default)] +pub struct TestAssetLoader; + +#[derive(Debug, Error)] +pub struct TestError; + +impl Display for TestError { + fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) + } +} + +impl bevy::asset::AssetLoader for TestAssetLoader { + type Asset = TestAsset; + type Settings = (); + type Error = TestError; + fn load<'a>( + &'a self, + reader: &'a mut bevy::asset::io::Reader, + _: &'a (), + _: &'a mut bevy::asset::LoadContext, + ) -> bevy::utils::BoxedFuture<'a, Result> { + Box::pin(async move { + let mut bytes = Vec::new(); + bevy::asset::AsyncReadExt::read_to_end(reader, &mut bytes) + .await + .unwrap(); + + Ok(TestAsset { + value: String::from_utf8(bytes).unwrap(), + }) + }) + } + + fn extensions(&self) -> &[&str] { + &["test"] + } +} + +#[test] +fn work_with_embedded_source_plugin_before() { + let mut app = App::new(); + app.add_plugins(EmbeddedAssetPlugin { + mode: PluginMode::ReplaceDefault, + }) + .add_plugins(DefaultPlugins.set(AssetPlugin { + file_path: "test".to_string(), + ..default() + })) + .init_asset::() + .init_asset_loader::(); + app.finish(); + + let asset_server = app.world.resource_mut::(); + let handle_1: Handle = asset_server.load("example_asset.test"); + let handle_2: Handle = asset_server.load("açèt.test"); + let handle_3: Handle = asset_server.load("subdir/other_asset.test"); + app.update(); + let test_assets = app.world.resource_mut::>(); + let asset = test_assets.get(handle_1).unwrap(); + assert_eq!(asset.value, "hello"); + let asset = test_assets.get(handle_2).unwrap(); + assert_eq!(asset.value, "with special chars"); + let asset = test_assets.get(handle_3).unwrap(); + assert_eq!(asset.value, "in subdirectory"); +} + +#[test] +#[should_panic] +fn work_with_embedded_source_plugin_after() { + let mut app = App::new(); + app.add_plugins(DefaultPlugins.set(AssetPlugin { + file_path: "test".to_string(), + ..default() + })) + .add_plugins(EmbeddedAssetPlugin { + mode: PluginMode::ReplaceDefault, + }) + .init_asset::() + .init_asset_loader::(); + app.finish(); + + let asset_server = app.world.resource_mut::(); + let handle_1: Handle = asset_server.load("example_asset.test"); + app.update(); + let test_assets = app.world.resource_mut::>(); + test_assets.get(handle_1).unwrap(); +} + +// #[test] +// #[should_panic] +// fn doesnt_work_with_plugin() { +// let mut app = App::new(); +// app.add_plugins(DefaultPlugins) +// .init_asset::() +// .init_asset_loader::(); +// app.finish(); + +// let asset_server = app.world.resource_mut::(); +// let handle_1: Handle = asset_server.load("example_asset.test"); +// let handle_2: Handle = asset_server.load("açèt.test"); +// let handle_3: Handle = asset_server.load("subdir/other_asset.test"); +// app.update(); +// let test_assets = app.world.resource_mut::>(); +// let asset = test_assets.get(handle_1).unwrap(); +// assert_eq!(asset.value, "hello"); +// let asset = test_assets.get(handle_2).unwrap(); +// assert_eq!(asset.value, "with special chars"); +// let asset = test_assets.get(handle_3).unwrap(); +// assert_eq!(asset.value, "in subdirectory"); +// } diff --git a/tests/as_default_with_fallback.rs b/tests/as_default_with_fallback.rs new file mode 100644 index 0000000..ba7c5f4 --- /dev/null +++ b/tests/as_default_with_fallback.rs @@ -0,0 +1,83 @@ +#![cfg(feature = "default-source")] + +use std::fmt::Display; + +use bevy::{prelude::*, utils::thiserror::Error}; +use bevy_embedded_assets::{EmbeddedAssetPlugin, PluginMode}; + +#[derive(Asset, TypePath, Debug)] +pub struct TestAsset { + pub value: String, +} + +#[derive(Default)] +pub struct TestAssetLoader; + +#[derive(Debug, Error)] +pub struct TestError; + +impl Display for TestError { + fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) + } +} + +impl bevy::asset::AssetLoader for TestAssetLoader { + type Asset = TestAsset; + type Settings = (); + type Error = TestError; + fn load<'a>( + &'a self, + reader: &'a mut bevy::asset::io::Reader, + _: &'a (), + _: &'a mut bevy::asset::LoadContext, + ) -> bevy::utils::BoxedFuture<'a, Result> { + Box::pin(async move { + let mut bytes = Vec::new(); + bevy::asset::AsyncReadExt::read_to_end(reader, &mut bytes) + .await + .unwrap(); + + Ok(TestAsset { + value: String::from_utf8(bytes).unwrap(), + }) + }) + } + + fn extensions(&self) -> &[&str] { + &["test"] + } +} + +#[test] +fn work_with_embedded_source_plugin_before() { + let mut app = App::new(); + app.add_plugins(EmbeddedAssetPlugin { + mode: PluginMode::ReplaceAndFallback { + path: "runtime_assets".to_string(), + }, + }) + .add_plugins(DefaultPlugins.set(AssetPlugin { + file_path: "runtime_assets".to_string(), + ..default() + })) + .init_asset::() + .init_asset_loader::(); + app.finish(); + + let asset_server = app.world.resource_mut::(); + let handle_1: Handle = asset_server.load("example_asset.test"); + let handle_2: Handle = asset_server.load("açèt.test"); + let handle_3: Handle = asset_server.load("subdir/other_asset.test"); + let handle_4: Handle = asset_server.load("asset.test"); + app.update(); + let test_assets = app.world.resource_mut::>(); + let asset = test_assets.get(handle_1).unwrap(); + assert_eq!(asset.value, "hello"); + let asset = test_assets.get(handle_2).unwrap(); + assert_eq!(asset.value, "with special chars"); + let asset = test_assets.get(handle_3).unwrap(); + assert_eq!(asset.value, "in subdirectory"); + let asset = test_assets.get(handle_4).unwrap(); + assert_eq!(asset.value, "at runtime"); +} diff --git a/tests/embedded.rs b/tests/embedded.rs new file mode 100644 index 0000000..90e7e9d --- /dev/null +++ b/tests/embedded.rs @@ -0,0 +1,117 @@ +use std::fmt::Display; + +use bevy::{prelude::*, utils::thiserror::Error}; +use bevy_embedded_assets::EmbeddedAssetPlugin; + +#[derive(Asset, TypePath, Debug)] +pub struct TestAsset { + pub value: String, +} + +#[derive(Default)] +pub struct TestAssetLoader; + +#[derive(Debug, Error)] +pub struct TestError; + +impl Display for TestError { + fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) + } +} + +impl bevy::asset::AssetLoader for TestAssetLoader { + type Asset = TestAsset; + type Settings = (); + type Error = TestError; + fn load<'a>( + &'a self, + reader: &'a mut bevy::asset::io::Reader, + _: &'a (), + _: &'a mut bevy::asset::LoadContext, + ) -> bevy::utils::BoxedFuture<'a, Result> { + Box::pin(async move { + let mut bytes = Vec::new(); + bevy::asset::AsyncReadExt::read_to_end(reader, &mut bytes) + .await + .unwrap(); + + Ok(TestAsset { + value: String::from_utf8(bytes).unwrap(), + }) + }) + } + + fn extensions(&self) -> &[&str] { + &["test"] + } +} + +#[test] +fn work_with_embedded_source_plugin_before() { + let mut app = App::new(); + app.add_plugins(EmbeddedAssetPlugin::default()) + .add_plugins(DefaultPlugins) + .init_asset::() + .init_asset_loader::(); + app.finish(); + + let asset_server = app.world.resource_mut::(); + let handle_1: Handle = asset_server.load("embedded://example_asset.test"); + let handle_2: Handle = asset_server.load("embedded://açèt.test"); + let handle_3: Handle = asset_server.load("embedded://subdir/other_asset.test"); + app.update(); + let test_assets = app.world.resource_mut::>(); + let asset = test_assets.get(handle_1).unwrap(); + assert_eq!(asset.value, "hello"); + let asset = test_assets.get(handle_2).unwrap(); + assert_eq!(asset.value, "with special chars"); + let asset = test_assets.get(handle_3).unwrap(); + assert_eq!(asset.value, "in subdirectory"); +} + +#[test] +fn work_with_embedded_source_plugin_after() { + let mut app = App::new(); + app.add_plugins(DefaultPlugins) + .add_plugins(EmbeddedAssetPlugin::default()) + .init_asset::() + .init_asset_loader::(); + app.finish(); + + let asset_server = app.world.resource_mut::(); + let handle_1: Handle = asset_server.load("embedded://example_asset.test"); + let handle_2: Handle = asset_server.load("embedded://açèt.test"); + let handle_3: Handle = asset_server.load("embedded://subdir/other_asset.test"); + app.update(); + let test_assets = app.world.resource_mut::>(); + let asset = test_assets.get(handle_1).unwrap(); + assert_eq!(asset.value, "hello"); + let asset = test_assets.get(handle_2).unwrap(); + assert_eq!(asset.value, "with special chars"); + let asset = test_assets.get(handle_3).unwrap(); + assert_eq!(asset.value, "in subdirectory"); +} + +#[test] +#[should_panic] +fn doesnt_work_with_plugin() { + let mut app = App::new(); + app.add_plugins(DefaultPlugins) + .init_asset::() + .init_asset_loader::(); + app.finish(); + + let asset_server = app.world.resource_mut::(); + let handle_1: Handle = asset_server.load("embedded://example_asset.test"); + let handle_2: Handle = asset_server.load("embedded://açèt.test"); + let handle_3: Handle = asset_server.load("embedded://subdir/other_asset.test"); + app.update(); + let test_assets = app.world.resource_mut::>(); + let asset = test_assets.get(handle_1).unwrap(); + assert_eq!(asset.value, "hello"); + let asset = test_assets.get(handle_2).unwrap(); + assert_eq!(asset.value, "with special chars"); + let asset = test_assets.get(handle_3).unwrap(); + assert_eq!(asset.value, "in subdirectory"); +} diff --git a/tests/preloaded.rs b/tests/preloaded.rs deleted file mode 100644 index 1f730ba..0000000 --- a/tests/preloaded.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::path::Path; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen_test::wasm_bindgen_test; - -use bevy_embedded_assets::EmbeddedAssetIo; - -#[cfg_attr(not(target_arch = "wasm32"), test)] -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -fn check_preloaded_simple() { - let embedded = EmbeddedAssetIo::preloaded(); - - let path = "example_asset"; - - let loaded = embedded.load_path_sync(&Path::new(path)); - assert!(loaded.is_ok()); - let raw_asset = loaded.unwrap(); - assert!(String::from_utf8(raw_asset.clone()).is_ok()); - assert_eq!(String::from_utf8(raw_asset).unwrap(), "hello"); -} - -#[cfg_attr(not(target_arch = "wasm32"), test)] -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -fn check_preloaded_special_chars() { - let embedded = EmbeddedAssetIo::preloaded(); - - let path = "açèt"; - - let loaded = embedded.load_path_sync(&Path::new(path)); - assert!(loaded.is_ok()); - let raw_asset = loaded.unwrap(); - assert!(String::from_utf8(raw_asset.clone()).is_ok()); - assert_eq!(String::from_utf8(raw_asset).unwrap(), "with special chars"); -} - -#[cfg_attr(not(target_arch = "wasm32"), test)] -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -fn check_preloaded_subdir() { - let embedded = EmbeddedAssetIo::preloaded(); - - let path = "subdir/other_asset"; - - let loaded = embedded.load_path_sync(&Path::new(path)); - assert!(loaded.is_ok()); - let raw_asset = loaded.unwrap(); - assert!(String::from_utf8(raw_asset.clone()).is_ok()); - assert_eq!(String::from_utf8(raw_asset).unwrap(), "in subdirectory"); -}