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 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::<BlockVisuals>();
app.init_asset_loader::<BlockVisualsLoader>();
app.load_resource::<BlockVisualsCollection>();
app.init_manifest::<BlockVisuals>("blocks.manifest.ron");
app.add_observer(insert_block_visuals);
}
@ -28,6 +28,10 @@ struct BlockVisuals {
material: Handle<StandardMaterial>,
}
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<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"),
}
}
}
/// Automatically inserts a [`Block`]'s visual components ([`Mesh3d`]
/// and [`MeshMaterial3d`]) from its ID when it is spawned.
fn insert_block_visuals(
event: On<Add, Block>,
blocks: Query<&Block>,
visuals_collection: Res<BlockVisualsCollection>,
visuals_assets: Res<Assets<BlockVisuals>>,
manifest: Manifest<BlockVisuals>,
meshes: Res<BuiltinBlockMeshes>,
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<BlockVisuals>`.
let visuals = visuals_assets.get(visuals).unwrap();
commands.entity(block).insert((
Mesh3d(meshes.cube.clone()),

@ -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::<ResourceHandles>();
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<T: Resource + Asset + Clone>(&mut self, value: T) -> &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 {
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>();
let world = self.world_mut();
let value = T::from_world(world);
let assets = world.resource::<AssetServer>();
let handle = assets.add(value);
let handle = assets.add(value).untyped();
let mut handles = world.resource_mut::<ResourceHandles>();
handles
.waiting
.push_back((handle.untyped(), |world, handle| {
let assets = world.resource::<Assets<T>>();
if let Some(value) = assets.get(handle.id().typed::<T>()) {
world.insert_resource(value.clone());
}
}));
handles.waiting.push_back((handle, |world, handle| {
let assets = world.resource::<Assets<T>>();
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
}
}
@ -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<UntypedHandle>,
}
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()
}

@ -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::<BlockDefinition>();
app.init_asset_loader::<BlockDefinitionLoader>();
app.load_resource::<BlockDefinitionCollection>();
app.init_manifest::<BlockDefinition>("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<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 block_definition;
pub mod manifest;
pub fn plugin(app: &mut App) {
// `tracking::plugin` must come first.

@ -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<BlockPos, Entity>);
#[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<Res<'w, BlockDefinitionCollection>>,
_definition_assets: Res<'w, Assets<BlockDefinition>>,
manifest: Manifest<'w, BlockDefinition>,
blocks: Query<'w, 's, &'static Block>,
commands: Commands<'w, 's>,
}
@ -68,11 +65,7 @@ impl Blocks<'_, '_> {
pos: BlockPos,
id: Identifier<Block>,
) -> Result<EntityCommands, SpawnBlockError> {
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) {

@ -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<T> {
#[deref]
value: Cow<'static, str>,
#[reflect(ignore)]
identifier_type: PhantomData<T>,
identifier_type: PhantomData<fn(T)>,
}
impl<T> Identifier<T> {
@ -111,3 +114,21 @@ impl<T> TryFrom<&Path> for Identifier<T> {
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::assets::block_definition::*;
pub use crate::assets::manifest::*;
pub use crate::block::*;
pub use crate::identifier::*;
pub use crate::math::*;

Loading…
Cancel
Save