From acf266794480258b1e15307c86b71e41fec0bf17 Mon Sep 17 00:00:00 2001 From: copygirl Date: Mon, 22 Dec 2025 01:58:06 +0100 Subject: [PATCH] Load block assets from manifest - Add `load_resource_with` extension function which inserts a loadable resource by value without requiring implementing `FromWorld` trait. - Add singleton-like `ManifestResource` which holds a `ManifestAsset` of a given asset type. Once loaded, the resource is added, just like `load_resource` did previously. - Add a `Manifest` system param with a helper lookup function to make looking up entries easier as you'd otherwise need 3 system params. - Add a `InitManifest` extension trait for easy setup - Add a `blocks.manifest.ron` file that references all blocks - Implement `ManifestEntry` on `BlockDefinition` and `BlockVisuals` - Remove `BlockDefinitionCollection` and `BlockVisualsCollection` --- assets/blocks.manifest.ron | 4 + client/src/assets/block_visuals.rs | 48 +++--------- common/src/assets/asset_tracking.rs | 53 ++++++++----- common/src/assets/block_definition.rs | 40 ++-------- common/src/assets/manifest.rs | 109 ++++++++++++++++++++++++++ common/src/assets/mod.rs | 1 + common/src/block.rs | 15 +--- common/src/identifier.rs | 23 +++++- common/src/lib.rs | 1 + 9 files changed, 194 insertions(+), 100 deletions(-) create mode 100644 assets/blocks.manifest.ron create mode 100644 common/src/assets/manifest.rs diff --git a/assets/blocks.manifest.ron b/assets/blocks.manifest.ron new file mode 100644 index 0000000..08fba27 --- /dev/null +++ b/assets/blocks.manifest.ron @@ -0,0 +1,4 @@ +[ + "default", + "platform", +] diff --git a/client/src/assets/block_visuals.rs b/client/src/assets/block_visuals.rs index 7332424..461d66b 100644 --- a/client/src/assets/block_visuals.rs +++ b/client/src/assets/block_visuals.rs @@ -1,7 +1,8 @@ use bevy::prelude::*; use common::prelude::*; -use bevy::asset::{AssetLoader, LoadContext, io::Reader}; +use bevy::asset::io::Reader; +use bevy::asset::{AssetLoader, LoadContext}; use serde::Deserialize; pub fn plugin(app: &mut App) { @@ -9,9 +10,8 @@ pub fn plugin(app: &mut App) { let cube = app.world_mut().add_asset(mesh); app.insert_resource(BuiltinBlockMeshes { cube }); - app.init_asset::(); app.init_asset_loader::(); - app.load_resource::(); + app.init_manifest::("blocks.manifest.ron"); app.add_observer(insert_block_visuals); } @@ -28,6 +28,10 @@ struct BlockVisuals { material: Handle, } +impl ManifestEntry for BlockVisuals { + type Marker = Block; +} + #[derive(Deserialize)] struct BlockTypeVisualsRaw { color: (u8, u8, u8), @@ -68,52 +72,22 @@ impl AssetLoader for BlockVisualsLoader { } } -#[derive(Resource, Asset, Reflect, Clone)] -#[reflect(Resource)] -struct BlockVisualsCollection { - #[dependency] - default: Handle, - #[dependency] - platform: Handle, -} - -impl BlockVisualsCollection { - pub fn get(&self, id: &Identifier) -> Option<&Handle> { - match id.as_ref() { - "default" => Some(&self.default), - "platform" => Some(&self.platform), - _ => None, - } - } -} - -impl FromWorld for BlockVisualsCollection { - fn from_world(world: &mut World) -> Self { - let assets = world.resource::(); - Self { - default: assets.load("blocks/default.ron"), - platform: assets.load("blocks/platform.ron"), - } - } -} - +/// Automatically inserts a [`Block`]'s visual components ([`Mesh3d`] +/// and [`MeshMaterial3d`]) from its ID when it is spawned. fn insert_block_visuals( event: On, blocks: Query<&Block>, - visuals_collection: Res, - visuals_assets: Res>, + manifest: Manifest, meshes: Res, mut commands: Commands, ) { let block = event.entity; let id = blocks.get(block).unwrap().id(); - let Some(visuals) = visuals_collection.get(id) else { + let Some(visuals) = manifest.get(id) else { warn!("missing BlockVisuals for id `{id}`"); return; }; - // SAFETY: If it's in `BlockVisualsCollection`, it should be in `Assets`. - let visuals = visuals_assets.get(visuals).unwrap(); commands.entity(block).insert(( Mesh3d(meshes.cube.clone()), diff --git a/common/src/assets/asset_tracking.rs b/common/src/assets/asset_tracking.rs index 9db688a..a068e56 100644 --- a/common/src/assets/asset_tracking.rs +++ b/common/src/assets/asset_tracking.rs @@ -1,38 +1,55 @@ //! A high-level way to load collections of asset handles as resources. -// Taken from: https://github.com/TheBevyFlock/bevy_new_2d/blob/main/src/asset_tracking.rs + +// Taken from / inspired by / see also: +// https://taintedcoders.com/bevy/assets#advanced-asset-tracking +// https://github.com/TheBevyFlock/bevy_new_2d/blob/main/src/asset_tracking.rs +// https://github.com/janhohenheim/foxtrot/blob/main/src/asset_tracking.rs use std::collections::VecDeque; use bevy::prelude::*; +use bevy::asset::AssetPath; + pub(super) fn plugin(app: &mut App) { app.init_resource::(); app.add_systems(PreUpdate, load_resource_assets); } pub trait LoadResource { - /// This will load the [`Resource`] as an [`Asset`]. When all of its asset dependencies - /// have been loaded, it will be inserted as a resource. This ensures that the resource only - /// exists when the assets are ready. + fn load_resource_with(&mut self, value: T) -> &mut Self; + fn load_resource(&mut self) -> &mut Self; + + fn load_asset<'a, T: Asset>(&mut self, path: impl Into>) -> &mut Self; } impl LoadResource for App { - fn load_resource(&mut self) -> &mut Self { + fn load_resource_with(&mut self, value: T) -> &mut Self { self.init_asset::(); let world = self.world_mut(); - let value = T::from_world(world); let assets = world.resource::(); - let handle = assets.add(value); + let handle = assets.add(value).untyped(); let mut handles = world.resource_mut::(); - handles - .waiting - .push_back((handle.untyped(), |world, handle| { - let assets = world.resource::>(); - if let Some(value) = assets.get(handle.id().typed::()) { - world.insert_resource(value.clone()); - } - })); + handles.waiting.push_back((handle, |world, handle| { + let assets = world.resource::>(); + if let Some(value) = assets.get(handle.id().typed::()) { + world.insert_resource(value.clone()); + } + })); + self + } + + fn load_resource(&mut self) -> &mut Self { + let world = self.world_mut(); + let value = T::from_world(world); + self.load_resource_with(value) + } + + fn load_asset<'a, T: Asset>(&mut self, path: impl Into>) -> &mut Self { + let handle = self.world().load_asset::(path).untyped(); + let mut handles = self.world_mut().resource_mut::(); + handles.waiting.push_back((handle, |_, _| {})); self } } @@ -42,14 +59,14 @@ type InsertLoadedResource = fn(&mut World, &UntypedHandle); #[derive(Resource, Default)] pub struct ResourceHandles { - // Use a queue for waiting assets so they can be cycled through and moved to - // `finished` one at a time. + // Use a queue for waiting assets so they can be cycled + // through and moved to `finished` one at a time. waiting: VecDeque<(UntypedHandle, InsertLoadedResource)>, finished: Vec, } impl ResourceHandles { - /// Returns true if all requested [`Asset`]s have finished loading and are available as [`Resource`]s. + /// Returns true if all requested [`Asset`]s have finished loading. pub fn is_all_done(&self) -> bool { self.waiting.is_empty() } diff --git a/common/src/assets/block_definition.rs b/common/src/assets/block_definition.rs index 9ff450f..962ae8b 100644 --- a/common/src/assets/block_definition.rs +++ b/common/src/assets/block_definition.rs @@ -3,14 +3,13 @@ use bevy::prelude::*; use bevy::asset::{AssetLoader, LoadContext, io::Reader}; use serde::Deserialize; -use crate::assets::asset_tracking::LoadResource; +use crate::assets::manifest::*; use crate::block::Block; -use crate::prelude::Identifier; +use crate::identifier::Identifier; pub(super) fn plugin(app: &mut App) { - app.init_asset::(); app.init_asset_loader::(); - app.load_resource::(); + app.init_manifest::("blocks.manifest.ron"); } #[derive(Asset, TypePath)] @@ -24,6 +23,10 @@ impl BlockDefinition { } } +impl ManifestEntry for BlockDefinition { + type Marker = Block; +} + #[derive(Deserialize)] struct BlockDefinitionRaw { // ignore unknown fields @@ -57,32 +60,3 @@ impl AssetLoader for BlockDefinitionLoader { &["ron"] } } - -#[derive(Resource, Asset, Reflect, Clone)] -#[reflect(Resource)] -pub struct BlockDefinitionCollection { - #[dependency] - default: Handle, - #[dependency] - platform: Handle, -} - -impl BlockDefinitionCollection { - pub fn get(&self, id: &Identifier) -> Option<&Handle> { - match id.as_ref() { - "default" => Some(&self.default), - "platform" => Some(&self.platform), - _ => None, - } - } -} - -impl FromWorld for BlockDefinitionCollection { - fn from_world(world: &mut World) -> Self { - let assets = world.resource::(); - Self { - default: assets.load("blocks/default.ron"), - platform: assets.load("blocks/platform.ron"), - } - } -} diff --git a/common/src/assets/manifest.rs b/common/src/assets/manifest.rs new file mode 100644 index 0000000..f715f8d --- /dev/null +++ b/common/src/assets/manifest.rs @@ -0,0 +1,109 @@ +use std::marker::PhantomData; + +use bevy::prelude::*; + +use bevy::asset::io::Reader; +use bevy::asset::{AssetLoader, AssetPath, LoadContext}; +use bevy::ecs::system::SystemParam; +use derive_where::derive_where; + +use crate::assets::asset_tracking::LoadResource; +use crate::identifier::{Identifier, IdentifierMap}; + +/// Trait implemented by [`Asset`]s which can appear in a [`Manifest`]. +pub trait ManifestEntry: Asset { + type Marker: TypePath; +} + +#[derive(Resource, Asset, Reflect)] +#[derive_where(Clone)] +struct ManifestResource { + #[dependency] + handle: Handle>, +} + +#[derive(Asset, TypePath)] +struct ManifestAsset { + #[dependency] + map: IdentifierMap>, +} + +impl ManifestAsset { + pub fn get(&self, id: &Identifier) -> Option<&Handle> { + self.map.get(id) + } +} + +#[derive_where(Default)] +struct ManifestAssetLoader { + entry_type: PhantomData, +} + +impl AssetLoader for ManifestAssetLoader { + type Asset = ManifestAsset; + type Settings = (); + type Error = BevyError; + + async fn load( + &self, + reader: &mut dyn Reader, + _settings: &Self::Settings, + load_context: &mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let raw = ron::de::from_bytes::>(&bytes)?; + + let mut map = IdentifierMap::default(); + for id in raw { + let id = Identifier::::new(id)?; + let handle = load_context.load(format!("blocks/{id}.ron")); + map.try_insert(id, handle).map_err(|_| "duplicate entry")?; + } + Ok(ManifestAsset { map }) + } + + fn extensions(&self) -> &[&str] { + &["manifest.ron"] + } +} + +#[derive(SystemParam)] +pub struct Manifest<'w, T: ManifestEntry> { + resource: Option>>, + manifest_assets: Res<'w, Assets>>, + entry_assets: Res<'w, Assets>, +} + +impl Manifest<'_, T> { + /// Looks up an [`Asset`] of type `T` from its associated manifest. + /// + /// Returns `None` if an asset with the given identifier is missing from + /// the manifest, or if the manifest is not loaded, including if it has + /// not been initialized with [`InitManifest::init_manifest`]. + pub fn get(&self, id: &Identifier) -> Option<&T> { + let manifest_handle = &self.resource.as_ref()?.handle; + // SAFETY: Manifest loads this as a dependency, so it should exist. + let manifest = self.manifest_assets.get(manifest_handle).unwrap(); + let entry_handle = manifest.get(id)?; + // SAFETY: Entry loads this as a dependency, so it should exist. + let entry = self.entry_assets.get(entry_handle).unwrap(); + Some(entry) + } +} + +pub trait InitManifest { + fn init_manifest<'a, T: ManifestEntry>(&mut self, path: impl Into>); +} + +impl InitManifest for App { + fn init_manifest<'a, T: ManifestEntry>(&mut self, path: impl Into>) { + self.init_asset::(); + self.init_asset::>(); + self.init_asset_loader::>(); + + // Insert a `Resource` that holds a handle to the `ManifestAsset` once it's loaded. + let handle = self.world_mut().load_asset(path); + self.load_resource_with(ManifestResource:: { handle }); + } +} diff --git a/common/src/assets/mod.rs b/common/src/assets/mod.rs index e9ee507..1f2847c 100644 --- a/common/src/assets/mod.rs +++ b/common/src/assets/mod.rs @@ -2,6 +2,7 @@ use bevy::prelude::*; pub mod asset_tracking; pub mod block_definition; +pub mod manifest; pub fn plugin(app: &mut App) { // `tracking::plugin` must come first. diff --git a/common/src/block.rs b/common/src/block.rs index 31f8ee0..63c43f9 100644 --- a/common/src/block.rs +++ b/common/src/block.rs @@ -6,7 +6,8 @@ use bevy::platform::collections::HashMap; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::assets::block_definition::{BlockDefinition, BlockDefinitionCollection}; +use crate::assets::block_definition::BlockDefinition; +use crate::assets::manifest::Manifest; use crate::identifier::Identifier; use crate::math::BlockPos; @@ -41,11 +42,7 @@ pub struct BlockMap(HashMap); #[derive(SystemParam)] pub struct Blocks<'w, 's> { map: Res<'w, BlockMap>, - // NOTE: `Option` is used because systems using this parameter might run - // before block definitions are loaded, since they might not have - // a concept of when gameplay is started, for example. - definition_collection: Option>, - _definition_assets: Res<'w, Assets>, + manifest: Manifest<'w, BlockDefinition>, blocks: Query<'w, 's, &'static Block>, commands: Commands<'w, 's>, } @@ -68,11 +65,7 @@ impl Blocks<'_, '_> { pos: BlockPos, id: Identifier, ) -> Result { - let definitions = self - .definition_collection - .as_deref() - .expect("attempted to spawn block before definitions are loaded"); - if definitions.get(&id).is_none() { + if self.manifest.get(&id).is_none() { return Err(SpawnBlockError::UnknownBlockId(id)); } if let Some(existing) = self.get(pos) { diff --git a/common/src/identifier.rs b/common/src/identifier.rs index 72ea079..437ebb0 100644 --- a/common/src/identifier.rs +++ b/common/src/identifier.rs @@ -4,7 +4,10 @@ use std::path::Path; use bevy::prelude::*; +use bevy::asset::{UntypedAssetId, VisitAssetDependencies}; +use bevy::platform::collections::HashMap; use derive_where::derive_where; +use serde::{Deserialize, Serialize}; use thiserror::Error; #[derive(Reflect, Deref)] @@ -14,7 +17,7 @@ pub struct Identifier { #[deref] value: Cow<'static, str>, #[reflect(ignore)] - identifier_type: PhantomData, + identifier_type: PhantomData, } impl Identifier { @@ -111,3 +114,21 @@ impl TryFrom<&Path> for Identifier { Self::new(id) } } + +#[derive(Deserialize, Serialize, Reflect, Deref, DerefMut)] +#[derive_where(Default)] +pub struct IdentifierMap(HashMap, V>); + +impl Clone for IdentifierMap { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl VisitAssetDependencies for IdentifierMap> { + fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { + for dependency in self.values() { + visit(dependency.id().untyped()); + } + } +} diff --git a/common/src/lib.rs b/common/src/lib.rs index 9affcf8..735b33c 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -9,6 +9,7 @@ pub mod prelude { pub use crate::network; pub use crate::assets::block_definition::*; + pub use crate::assets::manifest::*; pub use crate::block::*; pub use crate::identifier::*; pub use crate::math::*;