bevy_assets_extensions 0.7.0

Extensions for bevy assets, with support of collection as assets and a loader manager.
Documentation
//! Module for assets bundle asset.

use core::str;

use bevy::{
    asset::{AssetLoader, ErasedLoadedAsset},
    platform::collections::HashMap,
    prelude::*,
};

use crate::prelude::*;

mod bundle_entry;
mod call_load_asset;
mod load_asset;

pub use bundle_entry::BundleEntry;
pub use call_load_asset::{
    AssetHandleWrapper, CallLoadAssetFrom, CallLoadFromWrapper, call_load_from, load_handle_from,
};
pub use load_asset::LoadAssetFrom;

/// Bundle of assets
#[derive(Asset, TypePath)]
pub struct AssetsBundle {
    assets: HashMap<String, ErasedLoadedAsset>,
}

impl std::fmt::Debug for AssetsBundle {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("AssetsBundle")
            .field("assets", &self.assets.keys())
            .finish()
    }
}

impl AssetsBundle {
    /// Return true if an asset with the given key exists.
    pub fn has_asset(&self, key: &str) -> bool {
        self.assets.contains_key(key)
    }
    /// Retrieve an asset for the given key, and cast it to the asset type.
    /// Return an error if the key does not exists, or if the asset cannot
    /// be converted.
    pub fn get_asset<'a, A: Asset>(&'a self, key: &str) -> Result<&'a A> {
        let loaded_asset = self
            .assets
            .get(key)
            .ok_or_else(|| Error::UnknownAssetKey { key: key.into() })?;

        loaded_asset.get::<A>().ok_or_else(|| {
            Error::InvalidAssetCast {
                expected: A::short_type_path(),
                got: loaded_asset.asset_type_name(),
            }
            .into()
        })
    }
}

#[derive(Default)]
pub(crate) struct AssetsBundleLoader;

impl AssetLoader for AssetsBundleLoader {
    type Asset = AssetsBundle;
    type Settings = ();
    type Error = Error;
    async fn load(
        &self,
        reader: &mut dyn bevy::asset::io::Reader,
        _: &Self::Settings,
        load_context: &mut bevy::asset::LoadContext<'_>,
    ) -> std::result::Result<Self::Asset, Self::Error> {
        let mut bytes = Vec::new();
        reader.read_to_end(&mut bytes).await?;

        let assets: HashMap<String, BundleEntry> = ron::from_str(str::from_utf8(&bytes)?)?;
        call_load_from!(
            AssetsBundle,
            "<FILE>".to_string(),
            BundleEntry::Bundle(assets),
            load_context
        )
        .await
    }
    fn extensions(&self) -> &[&str] {
        &["bundle.ron"]
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use bevy::asset::LoadState;
    use std::time::Duration;

    fn is_loading(load_state: LoadState) -> bool {
        matches!(load_state, LoadState::Loading)
    }
    fn check_is_loaded(load_state: LoadState) {
        match load_state {
            LoadState::Loaded => (),
            LoadState::Failed(f) => panic!("Failed to load: {}", f),
            _ => panic!("Not loaded."),
        }
    }

    #[test]
    fn test_bundle_asset_loading() {
        // Initialise test
        let mut app = App::new();
        app.add_plugins((MinimalPlugins, AssetPlugin::default()));
        app.init_asset::<AssetsBundle>();
        app.init_asset_loader::<AssetsBundleLoader>();
        app.init_asset::<assets::Text>();
        app.init_asset_loader::<assets::text::TextLoader>();

        // Start loading the test bundle
        let handle: Handle<AssetsBundle> = app
            .world()
            .resource::<AssetServer>()
            .load("test_relative/test.bundle.ron");
        app.update();

        // Wait for the loading to happen
        while is_loading(
            app.world()
                .resource::<AssetServer>()
                .get_load_state(&handle)
                .unwrap(),
        ) {
            app.update();
            std::thread::sleep(Duration::from_secs(1));
        }

        // Validate that the loading has happened
        check_is_loaded(
            app.world()
                .resource::<AssetServer>()
                .get_load_state(&handle)
                .unwrap(),
        );

        // Get the test bundle
        let test_bundle = app
            .world()
            .resource::<Assets<AssetsBundle>>()
            .get(&handle)
            .unwrap();

        // Validate "hello"
        assert!(test_bundle.has_asset("hello"));
        assert_eq!(
            test_bundle
                .get_asset::<assets::Text>("hello")
                .unwrap()
                .text_ref(),
            "hello"
        );

        // Validate "world"
        assert!(test_bundle.has_asset("world"));
        assert_eq!(
            test_bundle
                .get_asset::<assets::Text>("world")
                .unwrap()
                .text_ref(),
            "world"
        );

        // Validate "text"
        assert!(test_bundle.has_asset("text"));
        assert_eq!(
            test_bundle
                .get_asset::<assets::Text>("text")
                .unwrap()
                .text_ref(),
            "Hello World!"
        );
    }

    #[test]
    fn test_bundle_with_bundle_asset_loading() {
        // Initialise test
        let mut app = App::new();
        app.add_plugins((MinimalPlugins, AssetPlugin::default()));
        app.init_asset::<AssetsBundle>();
        app.init_asset_loader::<AssetsBundleLoader>();
        app.init_asset::<assets::Text>();
        app.init_asset_loader::<assets::text::TextLoader>();

        // Start loading the test bundle
        let handle: Handle<AssetsBundle> = app
            .world()
            .resource::<AssetServer>()
            .load("test_bundle.bundle.ron");
        app.update();

        // Wait for the loading to happen
        while is_loading(
            app.world()
                .resource::<AssetServer>()
                .get_load_state(&handle)
                .unwrap(),
        ) {
            app.update();
            std::thread::sleep(Duration::from_secs(1));
        }

        // Validate that the loading has happened
        check_is_loaded(
            app.world()
                .resource::<AssetServer>()
                .get_load_state(&handle)
                .unwrap(),
        );

        // Get the test bundle
        let test_bundle = app
            .world()
            .resource::<Assets<AssetsBundle>>()
            .get(&handle)
            .unwrap();

        // Validate "hello"
        assert!(test_bundle.has_asset("hello"));
        assert_eq!(
            test_bundle
                .get_asset::<assets::Text>("hello")
                .unwrap()
                .text_ref(),
            "hello"
        );

        // Get the subtest bundle
        assert!(test_bundle.has_asset("world"));
        let test_bundle = test_bundle.get_asset::<AssetsBundle>("world").unwrap();

        // Validate "hello"
        assert!(test_bundle.has_asset("hello"));
        assert_eq!(
            test_bundle
                .get_asset::<assets::Text>("hello")
                .unwrap()
                .text_ref(),
            "hello"
        );

        // Validate "world"
        assert!(test_bundle.has_asset("world"));
        assert_eq!(
            test_bundle
                .get_asset::<assets::Text>("world")
                .unwrap()
                .text_ref(),
            "world"
        );

        // Validate "text"
        assert!(test_bundle.has_asset("text"));
        assert_eq!(
            test_bundle
                .get_asset::<assets::Text>("text")
                .unwrap()
                .text_ref(),
            "Hello World!"
        );
    }

    #[cfg(feature = "atlas_layout")]
    #[test]
    fn test_static_asset_atlast_layout_loading() {
        // Initialise test
        let mut app = App::new();
        app.add_plugins((MinimalPlugins, AssetPlugin::default()));
        app.init_asset::<AssetsBundle>();
        app.init_asset::<TextureAtlasLayout>();
        app.init_asset_loader::<AssetsBundleLoader>();
        app.init_asset_loader::<crate::assets::atlas_layout::TextureAtlasLayoutLoader>();

        // Start loading the test bundle
        let handle: Handle<AssetsBundle> = app
            .world()
            .resource::<AssetServer>()
            .load("with_atlas_layout.bundle.ron");
        app.update();

        // Wait for the loading to happen
        while is_loading(
            app.world()
                .resource::<AssetServer>()
                .get_load_state(&handle)
                .unwrap(),
        ) {
            app.update();
            std::thread::sleep(Duration::from_secs(1));
        }

        // Get the test bundle
        let test_bundle = app
            .world()
            .resource::<Assets<AssetsBundle>>()
            .get(&handle)
            .unwrap();

        // Validate "layout"
        assert!(test_bundle.has_asset("layout"));
        let texture_atlas = test_bundle
            .get_asset::<TextureAtlasLayout>("layout")
            .unwrap();

        assert_eq!(texture_atlas.size, UVec2::new(192, 297));
        assert_eq!(
            texture_atlas.textures,
            vec![
                URect::new(0, 0, 96, 99),
                URect::new(96, 0, 192, 99),
                URect::new(0, 99, 96, 198),
                URect::new(96, 99, 192, 198),
                URect::new(0, 198, 96, 297),
                URect::new(96, 198, 192, 297),
            ]
        );
    }
}