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`
main
copygirl 3 days ago
parent 7e0dfd8fdd
commit acf2667944
  1. 4
      assets/blocks.manifest.ron
  2. 48
      client/src/assets/block_visuals.rs
  3. 53
      common/src/assets/asset_tracking.rs
  4. 40
      common/src/assets/block_definition.rs
  5. 109
      common/src/assets/manifest.rs
  6. 1
      common/src/assets/mod.rs
  7. 15
      common/src/block.rs
  8. 23
      common/src/identifier.rs
  9. 1
      common/src/lib.rs

@ -0,0 +1,4 @@
[
"default",
"platform",
]

@ -1,7 +1,8 @@
use bevy::prelude::*; use bevy::prelude::*;
use common::prelude::*; use common::prelude::*;
use bevy::asset::{AssetLoader, LoadContext, io::Reader}; use bevy::asset::io::Reader;
use bevy::asset::{AssetLoader, LoadContext};
use serde::Deserialize; use serde::Deserialize;
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
@ -9,9 +10,8 @@ pub fn plugin(app: &mut App) {
let cube = app.world_mut().add_asset(mesh); let cube = app.world_mut().add_asset(mesh);
app.insert_resource(BuiltinBlockMeshes { cube }); app.insert_resource(BuiltinBlockMeshes { cube });
app.init_asset::<BlockVisuals>();
app.init_asset_loader::<BlockVisualsLoader>(); app.init_asset_loader::<BlockVisualsLoader>();
app.load_resource::<BlockVisualsCollection>(); app.init_manifest::<BlockVisuals>("blocks.manifest.ron");
app.add_observer(insert_block_visuals); app.add_observer(insert_block_visuals);
} }
@ -28,6 +28,10 @@ struct BlockVisuals {
material: Handle<StandardMaterial>, material: Handle<StandardMaterial>,
} }
impl ManifestEntry for BlockVisuals {
type Marker = Block;
}
#[derive(Deserialize)] #[derive(Deserialize)]
struct BlockTypeVisualsRaw { struct BlockTypeVisualsRaw {
color: (u8, u8, u8), color: (u8, u8, u8),
@ -68,52 +72,22 @@ impl AssetLoader for BlockVisualsLoader {
} }
} }
#[derive(Resource, Asset, Reflect, Clone)] /// Automatically inserts a [`Block`]'s visual components ([`Mesh3d`]
#[reflect(Resource)] /// and [`MeshMaterial3d`]) from its ID when it is spawned.
struct BlockVisualsCollection {
#[dependency]
default: Handle<BlockVisuals>,
#[dependency]
platform: Handle<BlockVisuals>,
}
impl BlockVisualsCollection {
pub fn get(&self, id: &Identifier<Block>) -> Option<&Handle<BlockVisuals>> {
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::<AssetServer>();
Self {
default: assets.load("blocks/default.ron"),
platform: assets.load("blocks/platform.ron"),
}
}
}
fn insert_block_visuals( fn insert_block_visuals(
event: On<Add, Block>, event: On<Add, Block>,
blocks: Query<&Block>, blocks: Query<&Block>,
visuals_collection: Res<BlockVisualsCollection>, manifest: Manifest<BlockVisuals>,
visuals_assets: Res<Assets<BlockVisuals>>,
meshes: Res<BuiltinBlockMeshes>, meshes: Res<BuiltinBlockMeshes>,
mut commands: Commands, mut commands: Commands,
) { ) {
let block = event.entity; let block = event.entity;
let id = blocks.get(block).unwrap().id(); 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}`"); warn!("missing BlockVisuals for id `{id}`");
return; return;
}; };
// SAFETY: If it's in `BlockVisualsCollection`, it should be in `Assets<BlockVisuals>`.
let visuals = visuals_assets.get(visuals).unwrap();
commands.entity(block).insert(( commands.entity(block).insert((
Mesh3d(meshes.cube.clone()), Mesh3d(meshes.cube.clone()),

@ -1,38 +1,55 @@
//! A high-level way to load collections of asset handles as resources. //! 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 std::collections::VecDeque;
use bevy::prelude::*; use bevy::prelude::*;
use bevy::asset::AssetPath;
pub(super) fn plugin(app: &mut App) { pub(super) fn plugin(app: &mut App) {
app.init_resource::<ResourceHandles>(); app.init_resource::<ResourceHandles>();
app.add_systems(PreUpdate, load_resource_assets); app.add_systems(PreUpdate, load_resource_assets);
} }
pub trait LoadResource { pub trait LoadResource {
/// This will load the [`Resource`] as an [`Asset`]. When all of its asset dependencies fn load_resource_with<T: Resource + Asset + Clone>(&mut self, value: T) -> &mut Self;
/// 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<T: Resource + Asset + Clone + FromWorld>(&mut self) -> &mut Self; fn load_resource<T: Resource + Asset + Clone + FromWorld>(&mut self) -> &mut Self;
fn load_asset<'a, T: Asset>(&mut self, path: impl Into<AssetPath<'a>>) -> &mut Self;
} }
impl LoadResource for App { impl LoadResource for App {
fn load_resource<T: Resource + Asset + Clone + FromWorld>(&mut self) -> &mut Self { fn load_resource_with<T: Resource + Asset + Clone>(&mut self, value: T) -> &mut Self {
self.init_asset::<T>(); self.init_asset::<T>();
let world = self.world_mut(); let world = self.world_mut();
let value = T::from_world(world);
let assets = world.resource::<AssetServer>(); let assets = world.resource::<AssetServer>();
let handle = assets.add(value); let handle = assets.add(value).untyped();
let mut handles = world.resource_mut::<ResourceHandles>(); let mut handles = world.resource_mut::<ResourceHandles>();
handles handles.waiting.push_back((handle, |world, handle| {
.waiting let assets = world.resource::<Assets<T>>();
.push_back((handle.untyped(), |world, handle| { if let Some(value) = assets.get(handle.id().typed::<T>()) {
let assets = world.resource::<Assets<T>>(); world.insert_resource(value.clone());
if let Some(value) = assets.get(handle.id().typed::<T>()) { }
world.insert_resource(value.clone()); }));
} self
})); }
fn load_resource<T: Resource + Asset + Clone + FromWorld>(&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<AssetPath<'a>>) -> &mut Self {
let handle = self.world().load_asset::<T>(path).untyped();
let mut handles = self.world_mut().resource_mut::<ResourceHandles>();
handles.waiting.push_back((handle, |_, _| {}));
self self
} }
} }
@ -42,14 +59,14 @@ type InsertLoadedResource = fn(&mut World, &UntypedHandle);
#[derive(Resource, Default)] #[derive(Resource, Default)]
pub struct ResourceHandles { pub struct ResourceHandles {
// Use a queue for waiting assets so they can be cycled through and moved to // Use a queue for waiting assets so they can be cycled
// `finished` one at a time. // through and moved to `finished` one at a time.
waiting: VecDeque<(UntypedHandle, InsertLoadedResource)>, waiting: VecDeque<(UntypedHandle, InsertLoadedResource)>,
finished: Vec<UntypedHandle>, finished: Vec<UntypedHandle>,
} }
impl ResourceHandles { 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 { pub fn is_all_done(&self) -> bool {
self.waiting.is_empty() self.waiting.is_empty()
} }

@ -3,14 +3,13 @@ use bevy::prelude::*;
use bevy::asset::{AssetLoader, LoadContext, io::Reader}; use bevy::asset::{AssetLoader, LoadContext, io::Reader};
use serde::Deserialize; use serde::Deserialize;
use crate::assets::asset_tracking::LoadResource; use crate::assets::manifest::*;
use crate::block::Block; use crate::block::Block;
use crate::prelude::Identifier; use crate::identifier::Identifier;
pub(super) fn plugin(app: &mut App) { pub(super) fn plugin(app: &mut App) {
app.init_asset::<BlockDefinition>();
app.init_asset_loader::<BlockDefinitionLoader>(); app.init_asset_loader::<BlockDefinitionLoader>();
app.load_resource::<BlockDefinitionCollection>(); app.init_manifest::<BlockDefinition>("blocks.manifest.ron");
} }
#[derive(Asset, TypePath)] #[derive(Asset, TypePath)]
@ -24,6 +23,10 @@ impl BlockDefinition {
} }
} }
impl ManifestEntry for BlockDefinition {
type Marker = Block;
}
#[derive(Deserialize)] #[derive(Deserialize)]
struct BlockDefinitionRaw { struct BlockDefinitionRaw {
// ignore unknown fields // ignore unknown fields
@ -57,32 +60,3 @@ impl AssetLoader for BlockDefinitionLoader {
&["ron"] &["ron"]
} }
} }
#[derive(Resource, Asset, Reflect, Clone)]
#[reflect(Resource)]
pub struct BlockDefinitionCollection {
#[dependency]
default: Handle<BlockDefinition>,
#[dependency]
platform: Handle<BlockDefinition>,
}
impl BlockDefinitionCollection {
pub fn get(&self, id: &Identifier<Block>) -> Option<&Handle<BlockDefinition>> {
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::<AssetServer>();
Self {
default: assets.load("blocks/default.ron"),
platform: assets.load("blocks/platform.ron"),
}
}
}

@ -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<T: ManifestEntry> {
#[dependency]
handle: Handle<ManifestAsset<T>>,
}
#[derive(Asset, TypePath)]
struct ManifestAsset<T: ManifestEntry> {
#[dependency]
map: IdentifierMap<T::Marker, Handle<T>>,
}
impl<T: ManifestEntry> ManifestAsset<T> {
pub fn get(&self, id: &Identifier<T::Marker>) -> Option<&Handle<T>> {
self.map.get(id)
}
}
#[derive_where(Default)]
struct ManifestAssetLoader<T: ManifestEntry> {
entry_type: PhantomData<T>,
}
impl<T: ManifestEntry> AssetLoader for ManifestAssetLoader<T> {
type Asset = ManifestAsset<T>;
type Settings = ();
type Error = BevyError;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &Self::Settings,
load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let raw = ron::de::from_bytes::<Vec<String>>(&bytes)?;
let mut map = IdentifierMap::default();
for id in raw {
let id = Identifier::<T::Marker>::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<Res<'w, ManifestResource<T>>>,
manifest_assets: Res<'w, Assets<ManifestAsset<T>>>,
entry_assets: Res<'w, Assets<T>>,
}
impl<T: ManifestEntry> 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<T::Marker>) -> 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<AssetPath<'a>>);
}
impl InitManifest for App {
fn init_manifest<'a, T: ManifestEntry>(&mut self, path: impl Into<AssetPath<'a>>) {
self.init_asset::<T>();
self.init_asset::<ManifestAsset<T>>();
self.init_asset_loader::<ManifestAssetLoader<T>>();
// 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::<T> { handle });
}
}

@ -2,6 +2,7 @@ use bevy::prelude::*;
pub mod asset_tracking; pub mod asset_tracking;
pub mod block_definition; pub mod block_definition;
pub mod manifest;
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
// `tracking::plugin` must come first. // `tracking::plugin` must come first.

@ -6,7 +6,8 @@ use bevy::platform::collections::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; 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::identifier::Identifier;
use crate::math::BlockPos; use crate::math::BlockPos;
@ -41,11 +42,7 @@ pub struct BlockMap(HashMap<BlockPos, Entity>);
#[derive(SystemParam)] #[derive(SystemParam)]
pub struct Blocks<'w, 's> { pub struct Blocks<'w, 's> {
map: Res<'w, BlockMap>, map: Res<'w, BlockMap>,
// NOTE: `Option` is used because systems using this parameter might run manifest: Manifest<'w, BlockDefinition>,
// before block definitions are loaded, since they might not have
// a concept of when gameplay is started, for example.
definition_collection: Option<Res<'w, BlockDefinitionCollection>>,
_definition_assets: Res<'w, Assets<BlockDefinition>>,
blocks: Query<'w, 's, &'static Block>, blocks: Query<'w, 's, &'static Block>,
commands: Commands<'w, 's>, commands: Commands<'w, 's>,
} }
@ -68,11 +65,7 @@ impl Blocks<'_, '_> {
pos: BlockPos, pos: BlockPos,
id: Identifier<Block>, id: Identifier<Block>,
) -> Result<EntityCommands, SpawnBlockError> { ) -> Result<EntityCommands, SpawnBlockError> {
let definitions = self if self.manifest.get(&id).is_none() {
.definition_collection
.as_deref()
.expect("attempted to spawn block before definitions are loaded");
if definitions.get(&id).is_none() {
return Err(SpawnBlockError::UnknownBlockId(id)); return Err(SpawnBlockError::UnknownBlockId(id));
} }
if let Some(existing) = self.get(pos) { if let Some(existing) = self.get(pos) {

@ -4,7 +4,10 @@ use std::path::Path;
use bevy::prelude::*; use bevy::prelude::*;
use bevy::asset::{UntypedAssetId, VisitAssetDependencies};
use bevy::platform::collections::HashMap;
use derive_where::derive_where; use derive_where::derive_where;
use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
#[derive(Reflect, Deref)] #[derive(Reflect, Deref)]
@ -14,7 +17,7 @@ pub struct Identifier<T> {
#[deref] #[deref]
value: Cow<'static, str>, value: Cow<'static, str>,
#[reflect(ignore)] #[reflect(ignore)]
identifier_type: PhantomData<T>, identifier_type: PhantomData<fn(T)>,
} }
impl<T> Identifier<T> { impl<T> Identifier<T> {
@ -111,3 +114,21 @@ impl<T> TryFrom<&Path> for Identifier<T> {
Self::new(id) Self::new(id)
} }
} }
#[derive(Deserialize, Serialize, Reflect, Deref, DerefMut)]
#[derive_where(Default)]
pub struct IdentifierMap<T, V>(HashMap<Identifier<T>, V>);
impl<T, V: Clone> Clone for IdentifierMap<T, V> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T, A: Asset> VisitAssetDependencies for IdentifierMap<T, Handle<A>> {
fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
for dependency in self.values() {
visit(dependency.id().untyped());
}
}
}

@ -9,6 +9,7 @@ pub mod prelude {
pub use crate::network; pub use crate::network;
pub use crate::assets::block_definition::*; pub use crate::assets::block_definition::*;
pub use crate::assets::manifest::*;
pub use crate::block::*; pub use crate::block::*;
pub use crate::identifier::*; pub use crate::identifier::*;
pub use crate::math::*; pub use crate::math::*;

Loading…
Cancel
Save