diff --git a/Cargo.lock b/Cargo.lock index 8b8bf31649..902d27aa51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2208,6 +2208,14 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +[[package]] +name = "demo_asset_packs" +version = "0.4.0" +dependencies = [ + "bones_bevy_renderer", + "bones_framework", +] + [[package]] name = "demo_assets_minimal" version = "0.4.0" diff --git a/demos/asset_packs/Cargo.toml b/demos/asset_packs/Cargo.toml new file mode 100644 index 0000000000..c4b250027d --- /dev/null +++ b/demos/asset_packs/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "demo_asset_packs" +edition.workspace = true +version.workspace = true +license.workspace = true +publish = false + +[dependencies] +bones_framework = { path = "../../framework_crates/bones_framework" } +bones_bevy_renderer = { path = "../../framework_crates/bones_bevy_renderer" } diff --git a/demos/asset_packs/assets/game.yaml b/demos/asset_packs/assets/game.yaml new file mode 100644 index 0000000000..518aa5789b --- /dev/null +++ b/demos/asset_packs/assets/game.yaml @@ -0,0 +1,4 @@ +# This is our root asset file, which corresponds to our `GameMeta` struct. + +title: Asset Packs +core_items: [1, 2, 3] diff --git a/demos/asset_packs/assets/pack.yaml b/demos/asset_packs/assets/pack.yaml new file mode 100644 index 0000000000..e28802e90b --- /dev/null +++ b/demos/asset_packs/assets/pack.yaml @@ -0,0 +1,4 @@ +# This is the core asset pack file. It's only job is to specify +# the path to the root asset. + +root: ./game.yaml diff --git a/demos/asset_packs/packs/pack1/data.yaml b/demos/asset_packs/packs/pack1/data.yaml new file mode 100644 index 0000000000..a812e8f00f --- /dev/null +++ b/demos/asset_packs/packs/pack1/data.yaml @@ -0,0 +1,3 @@ +# This is our root asset file, which corresponds to our `PackMeta` struct. + +items: [10, 11, 12] diff --git a/demos/asset_packs/packs/pack1/pack.yaml b/demos/asset_packs/packs/pack1/pack.yaml new file mode 100644 index 0000000000..a68a8d7e39 --- /dev/null +++ b/demos/asset_packs/packs/pack1/pack.yaml @@ -0,0 +1,9 @@ +# This is the core asset pack file. It's only job is to specify +# the path to the root asset. + +name: Pack 1 +id: pack1_01J9GBGEDY07CT16AQYPKJRKCY +version: 0.1.0 +game_version: 0.1.0 +root: ./data.yaml +schemas: [] diff --git a/demos/asset_packs/packs/pack2/data.yaml b/demos/asset_packs/packs/pack2/data.yaml new file mode 100644 index 0000000000..a3a1653142 --- /dev/null +++ b/demos/asset_packs/packs/pack2/data.yaml @@ -0,0 +1,3 @@ +# This is our root asset file, which corresponds to our `PackMeta` struct. + +items: [21, 22, 23] diff --git a/demos/asset_packs/packs/pack2/pack.yaml b/demos/asset_packs/packs/pack2/pack.yaml new file mode 100644 index 0000000000..51388a9777 --- /dev/null +++ b/demos/asset_packs/packs/pack2/pack.yaml @@ -0,0 +1,9 @@ +# This is the core asset pack file. It's only job is to specify +# the path to the root asset. + +name: Pack 2 +id: pack2_01J9GBGEDYEAX5DMWGNJM9356T +version: 0.1.0 +game_version: 0.1.0 +root: ./data.yaml +schemas: [] diff --git a/demos/asset_packs/src/main.rs b/demos/asset_packs/src/main.rs new file mode 100644 index 0000000000..e2fb6fce95 --- /dev/null +++ b/demos/asset_packs/src/main.rs @@ -0,0 +1,86 @@ +use bones_bevy_renderer::BonesBevyRenderer; +use bones_framework::prelude::*; + +// +// NOTE: You must run this example from within the `demos/asset_packs` folder. Also, be sure to +// look at the `assets/` and `packs/` folders to see the asset files for this example. +// + +/// Our "core" asset type. +#[derive(HasSchema, Clone, Default)] +#[repr(C)] +// We must mark this as a metadata asset, and we set the type to "game". +// +// This means that any files with names like `game.yaml`, `game.yml`, `game.json`, `name.game.yaml`, +// etc. will be loaded as a `GameMeta` asset. +#[type_data(metadata_asset("game"))] +struct GameMeta { + title: String, + core_items: SVec, +} + +/// Our "supplementary" asset type. +#[derive(HasSchema, Clone, Default)] +#[repr(C)] +#[type_data(metadata_asset("data"))] +struct PackMeta { + items: SVec, +} + +fn main() { + // Setup logging + setup_logs!(); + + // First create bones game. + let mut game = Game::new(); + + game + // We initialize the asset server. + .init_shared_resource::(); + + // We must register all of our asset types before they can be loaded by the asset server. This + // may be done by calling schema() on each of our types, to register them with the schema + // registry. + GameMeta::register_schema(); + PackMeta::register_schema(); + + // Create a new session for the game menu. Each session is it's own bones world with it's own + // plugins, systems, and entities. + let menu_session = game.sessions.create("menu"); + menu_session + // Install the default bones_framework plugin for this session + .install_plugin(DefaultSessionPlugin) + // Add our menu system to the update stage + .add_system_to_stage(Update, menu_system); + + BonesBevyRenderer::new(game).app().run(); +} + +/// System to render the home menu. +fn menu_system( + egui_ctx: Res, + core_meta: Root, + all_packs: AllPacksData, +) { + egui::CentralPanel::default() + .frame(egui::Frame::none().outer_margin(egui::Margin::same(32.0))) + .show(&egui_ctx, |ui| { + // Use the title that has been loaded from the asset + ui.heading(&core_meta.title); + + ui.separator(); + + ui.label(egui::RichText::new("Items from all asset packs:")); + + // Show the numbers from all of the asset packs + egui::Grid::new("pack-items").num_columns(1).show(ui, |ui| { + for item in all_packs.iter_with( + |core| core.core_items.iter().copied(), + |pack| pack.items.iter().copied(), + ) { + ui.label(item.to_string()); + ui.end_row(); + } + }); + }); +} diff --git a/framework_crates/bones_framework/src/params.rs b/framework_crates/bones_framework/src/params.rs index 9dba2b85ab..109885221f 100644 --- a/framework_crates/bones_framework/src/params.rs +++ b/framework_crates/bones_framework/src/params.rs @@ -1,16 +1,23 @@ //! Bones ECS system parameters. +use std::{cell::RefCell, pin::Pin}; + use crate::prelude::*; -use dashmap::mapref::one::MappedRef; +type DashmapRef<'a, T> = dashmap::mapref::one::MappedRef<'a, Cid, LoadedAsset, T>; + +type DashmapIter<'a, K, V> = dashmap::iter::Iter<'a, K, V>; + /// Get the root asset of the core asset pack and cast it to type `T`. -pub struct Root<'a, T: HasSchema>(MappedRef<'a, Cid, LoadedAsset, T>); +pub struct Root<'a, T: HasSchema>(DashmapRef<'a, T>); + impl<'a, T: HasSchema> std::ops::Deref for Root<'a, T> { type Target = T; fn deref(&self) -> &Self::Target { &self.0 } } + impl<'a, T: HasSchema> SystemParam for Root<'a, T> { type State = AssetServer; type Param<'s> = Root<'s, T>; @@ -22,3 +29,275 @@ impl<'a, T: HasSchema> SystemParam for Root<'a, T> { Root(asset_server.root()) } } + +/// A helper system param for iterating over the root assets of the (non-core) asset packs, each +/// casted to type `T`. +/// +/// Asset packs contain a root asset in the form of an untyped asset handle. Use the +/// [`iter`][Self::iter] method to get an iterator over all asset packs. +/// +/// ## Example +/// +/// ```rust +/// use bones_framework::prelude::*; +/// use tracing::info; +/// +/// #[derive(Clone, Default, HasSchema)] +/// #[type_data(metadata_asset("root"))] +/// #[repr(C)] +/// struct PackMeta { +/// name: String, +/// } +/// +/// // Log the names of all non-core asset packs. +/// fn test(packs: Packs) -> Vec { +/// let mut names = Vec::new(); +/// for pack in packs.iter() { +/// names.push(pack.name.clone()); +/// } +/// names +/// } +/// +/// // Make sure that `Packs` is a valid system param. +/// IntoSystem::system(test); +/// ``` +/// +pub struct Packs<'a, T> { + asset_server: AssetServer, + _pack_t: std::marker::PhantomData<&'a T>, +} + +impl<'a, T: HasSchema> SystemParam for Packs<'a, T> { + type State = AssetServer; + type Param<'s> = Packs<'s, T>; + + fn get_state(world: &World) -> Self::State { + (*world.resource::()).clone() + } + + fn borrow<'s>(_world: &'s World, state: &'s mut Self::State) -> Self::Param<'s> { + Packs { + asset_server: state.clone(), + _pack_t: std::marker::PhantomData, + } + } +} + +impl Packs<'_, T> { + /// Get the typed asset pack roots iterator. + pub fn iter(&self) -> PacksIter { + PacksIter { + asset_server: &self.asset_server, + asset_packs_iter: self.asset_server.packs().iter(), + _pack_t: std::marker::PhantomData, + } + } +} + +/// A typed iterator over asset pack roots. +pub struct PacksIter<'a, T> { + asset_server: &'a AssetServer, + asset_packs_iter: DashmapIter<'a, AssetPackSpec, AssetPack>, + _pack_t: std::marker::PhantomData<&'a T>, +} + +impl<'a, T: HasSchema> Iterator for PacksIter<'a, T> { + type Item = DashmapRef<'a, T>; + + fn next(&mut self) -> Option { + let pack = self.asset_packs_iter.next()?; + let pack_root = self.asset_server.get(pack.root.typed::()); + Some(pack_root) + } +} + +/// A helper system param that allows for iteration over data contained in the +/// core game asset pack (the one in the `assets/` directory) and the supplementary asset +/// packs (the sub-directories of the `packs/` directory). +/// +/// This is intended for use with lists contained in the asset packs. For example, the core and +/// supplementary asset packs may contain lists of characters that users can choose to play as. +/// This system param may be used to iterate all of the characters available in all of the asset +/// packs. +/// +/// ## Example +/// +/// ```rust +/// use bones_framework::prelude::*; +/// use tracing::info; +/// +/// #[derive(Clone, Default, HasSchema)] +/// #[type_data(metadata_asset("root"))] +/// #[repr(C)] +/// struct GameMeta { +/// maps: SVec>, +/// } +/// +/// #[derive(Clone, Default, HasSchema)] +/// #[type_data(metadata_asset("pack"))] +/// #[repr(C)] +/// struct PackMeta { +/// maps: SVec>, +/// } +/// +/// #[derive(Clone, Default, HasSchema)] +/// #[type_data(metadata_asset("root"))] +/// struct MapMeta { +/// name: String, +/// } +/// +/// // Log the names of all maps in the core and other asset packs. +/// fn test(asset_server: Res, packs: AllPacksData) { +/// for handle in packs.iter_with( +/// |game: &GameMeta| game.maps.iter().copied(), +/// |pack: &PackMeta| pack.maps.iter().copied() +/// ) { +/// let map_meta = asset_server.get(handle); +/// info!(name = map_meta.name, "map"); +/// } +/// } +/// +/// // Make sure that `AllPacksData` is a valid system param. +/// IntoSystem::system(test); +/// ``` +/// +pub struct AllPacksData<'a, Core, Pack> +where + Core: HasSchema, + Pack: HasSchema, +{ + core_root: Root<'a, Core>, + pack_roots: Packs<'a, Pack>, +} + +impl<'a, Core, Pack> SystemParam for AllPacksData<'a, Core, Pack> +where + Core: HasSchema, + Pack: HasSchema, +{ + type State = ( + as SystemParam>::State, + as SystemParam>::State, + ); + type Param<'s> = AllPacksData<'s, Core, Pack>; + + fn get_state(world: &World) -> Self::State { + ( + Root::<'a, Core>::get_state(world), + Packs::<'a, Pack>::get_state(world), + ) + } + + fn borrow<'s>(world: &'s World, state: &'s mut Self::State) -> Self::Param<'s> { + AllPacksData { + core_root: Root::<'s, Core>::borrow(world, &mut state.0), + pack_roots: Packs::<'s, Pack>::borrow(world, &mut state.1), + } + } +} + +impl<'a, Core, Pack> AllPacksData<'a, Core, Pack> +where + Core: HasSchema, + Pack: HasSchema, +{ + /// Get the iterator over the core and supplementary asset packs. + /// + /// The first argument `core_accessor` is a function that must produce an iterator of `T` from + /// the core asset metadata. This is only called once, prior to iteration. + /// + /// Similarly, the second argument `pack_accessor` is a function that must produce an iterator + /// of `T` from a pack asset metadata. This is called once per pack, during iteration. + pub fn iter_with, PackItemIt: Iterator>( + &'a self, + mut core_accessor: impl FnMut(&'a Core) -> CoreItemIt, + pack_accessor: impl 'static + FnMut(&'a Pack) -> PackItemIt, + ) -> AllPacksDataIter<'a, T, CoreItemIt, Pack, PacksIter<'a, Pack>, PackItemIt> { + AllPacksDataIter { + core_item_iter: core_accessor(&*self.core_root), + pack_iter: self.pack_roots.iter(), + pack_accessor: Box::new(pack_accessor), + current_pack: AllPacksDataCurrentPack::new(), + } + } +} + +/// A flattened iterator of items of type `T` from data within the core and supplementary asset +/// packs. Items are first yielded from the core asset pack until exhausted, then items are yielded +/// from the supplementary asset packs, one at a time, and in no particular order. +/// +/// Can be acquired from [`AllPacksData::iter_with`] which takes two functions that produce the +/// inner iterator of items from the game meta and the inner iterators of items from the asset +/// packs, respectively. +/// +/// See [`AllPacksData`] for more info. +pub struct AllPacksDataIter<'a, T, CoreItemIt, Pack, PackIt, PackItemIt> { + core_item_iter: CoreItemIt, + pack_iter: PackIt, + pack_accessor: Box PackItemIt>, + current_pack: Pin>>>, +} + +struct AllPacksDataCurrentPack<'a, Pack, T, PackItemIt> { + pack: Option>, + item_iter: Option, + _marker: std::marker::PhantomData T>, +} + +impl<'a, Pack, T, PackItemIt> AllPacksDataCurrentPack<'a, Pack, T, PackItemIt> { + fn new() -> Pin>> { + Box::pin(RefCell::new(AllPacksDataCurrentPack { + pack: None, + item_iter: None, + _marker: std::marker::PhantomData, + })) + } + + fn set_pack( + &mut self, + next_pack: DashmapRef<'a, Pack>, + pack_accessor: &mut dyn FnMut(&'a Pack) -> PackItemIt, + ) -> &mut PackItemIt { + // Drop the item iterator + _ = self.item_iter.take(); + + // Set the pack + let pack = self.pack.insert(next_pack); + + // Setup the item iterator + let pack = unsafe { std::mem::transmute::<&Pack, &'a Pack>(pack) }; + let pack_item_iter = (pack_accessor)(pack); + self.item_iter.insert(pack_item_iter) + } +} + +impl<'a, Pack, T, CoreItemIt, PackIt, PackItemIt> Iterator + for AllPacksDataIter<'a, T, CoreItemIt, Pack, PackIt, PackItemIt> +where + CoreItemIt: Iterator, + PackIt: Iterator>, + PackItemIt: Iterator, +{ + type Item = T; + + fn next(&mut self) -> Option { + let next_core_item = self.core_item_iter.next(); + if next_core_item.is_some() { + return next_core_item; + } + + if let Some(iter) = self.current_pack.borrow_mut().item_iter.as_mut() { + let next_pack_item = iter.next(); + if next_pack_item.is_some() { + return next_pack_item; + } + } + + let next_pack = self.pack_iter.next()?; + + let mut current_pack = self.current_pack.borrow_mut(); + let current_pack_item_iter = current_pack.set_pack(next_pack, &mut self.pack_accessor); + + current_pack_item_iter.next() + } +} diff --git a/framework_crates/bones_framework/tests/assets/game.yaml b/framework_crates/bones_framework/tests/assets/game.yaml new file mode 100644 index 0000000000..730168fd19 --- /dev/null +++ b/framework_crates/bones_framework/tests/assets/game.yaml @@ -0,0 +1,2 @@ +data: abc +important_numbers: [1, 2, 3] diff --git a/framework_crates/bones_framework/tests/assets/pack.yaml b/framework_crates/bones_framework/tests/assets/pack.yaml new file mode 100644 index 0000000000..2fd5e136ad --- /dev/null +++ b/framework_crates/bones_framework/tests/assets/pack.yaml @@ -0,0 +1 @@ +root: game.yaml diff --git a/framework_crates/bones_framework/tests/packs/pack1/pack.yaml b/framework_crates/bones_framework/tests/packs/pack1/pack.yaml new file mode 100644 index 0000000000..1a077da865 --- /dev/null +++ b/framework_crates/bones_framework/tests/packs/pack1/pack.yaml @@ -0,0 +1,6 @@ +name: pack1 +id: pack1_01j9jyse2qf1sspywwfe0x1r5j +version: 0.0.0 +game_version: 0.0.0 +root: root.yaml +schemas: [] diff --git a/framework_crates/bones_framework/tests/packs/pack1/root.yaml b/framework_crates/bones_framework/tests/packs/pack1/root.yaml new file mode 100644 index 0000000000..c7daafb783 --- /dev/null +++ b/framework_crates/bones_framework/tests/packs/pack1/root.yaml @@ -0,0 +1,2 @@ +label: First Pack +other_numbers: [10, 20, 30] diff --git a/framework_crates/bones_framework/tests/packs/pack2/pack.yaml b/framework_crates/bones_framework/tests/packs/pack2/pack.yaml new file mode 100644 index 0000000000..aada0401b4 --- /dev/null +++ b/framework_crates/bones_framework/tests/packs/pack2/pack.yaml @@ -0,0 +1,6 @@ +name: pack2 +id: pack2_01j9msqxzjf38t26006w9xr0t5 +version: 0.0.0 +game_version: 0.0.0 +root: root.yaml +schemas: [] diff --git a/framework_crates/bones_framework/tests/packs/pack2/root.yaml b/framework_crates/bones_framework/tests/packs/pack2/root.yaml new file mode 100644 index 0000000000..55ffb14f94 --- /dev/null +++ b/framework_crates/bones_framework/tests/packs/pack2/root.yaml @@ -0,0 +1,2 @@ +label: Second Pack +other_numbers: [100, 200, 300] diff --git a/framework_crates/bones_framework/tests/system_param.rs b/framework_crates/bones_framework/tests/system_param.rs new file mode 100644 index 0000000000..250047ff5f --- /dev/null +++ b/framework_crates/bones_framework/tests/system_param.rs @@ -0,0 +1,113 @@ +use std::path::PathBuf; + +use bones_framework::prelude::*; +use futures_lite::{ + future::{block_on, yield_now}, + FutureExt, +}; + +fn create_world() -> World { + let mut world = World::default(); + + let core_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("assets"); + let packs_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("packs"); + let io = FileAssetIo::new(&core_dir, &packs_dir); + + let mut asset_server = world.init_resource::(); + asset_server.set_io(io); + + { + let scope = async move { + asset_server.load_assets().await.expect("load test assets"); + while !asset_server.load_progress.is_finished() { + yield_now().await; + } + }; + block_on(scope.boxed()); + } + + world +} + +#[derive(Clone, Default, HasSchema)] +#[type_data(metadata_asset("game"))] +#[repr(C)] +struct GameMeta { + data: String, + important_numbers: SVec, +} + +#[derive(Clone, Default, HasSchema)] +#[type_data(metadata_asset("root"))] +#[repr(C)] +struct PackMeta { + label: String, + other_numbers: SVec, +} + +fn init() { + static INIT: std::sync::Once = std::sync::Once::new(); + INIT.call_once(|| { + GameMeta::register_schema(); + PackMeta::register_schema(); + + bevy_tasks::IoTaskPool::init(|| bevy_tasks::TaskPoolBuilder::new().num_threads(1).build()); + setup_logs!(); + }); +} + +#[test] +fn core_root_data() { + init(); + let world = create_world(); + let actual = world.run_system(|root: Root| root.data.clone(), ()); + assert_eq!(actual, "abc".to_string()); +} + +#[test] +fn supplementary_packs_root_data() { + init(); + + let world = create_world(); + + let actual = world.run_system( + |packs: Packs| { + let mut data = packs + .iter() + .flat_map(|p| p.other_numbers.clone().into_iter()) + .collect::>(); + data.sort(); + data + }, + (), + ); + + assert_eq!(actual, vec![10, 20, 30, 100, 200, 300]); +} + +#[test] +fn all_packs_root_data() { + init(); + + let world = create_world(); + + let actual = world.run_system( + |packs: AllPacksData| { + let mut data = packs + .iter_with( + |core| core.important_numbers.iter().copied(), + |pack| pack.other_numbers.iter().copied(), + ) + .collect::>(); + data.sort(); + data + }, + (), + ); + + assert_eq!(actual, vec![1, 2, 3, 10, 20, 30, 100, 200, 300]); +}