diff --git a/Cargo.toml b/Cargo.toml index fd4d1833cd587..adc1360d8c43f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3032,6 +3032,21 @@ description = "Demonstrates how to use BSN to compose scenes" category = "Scene" wasm = true +[[example]] +name = "bsn_asset_catalog" +path = "examples/scene/bsn_asset_catalog.rs" +doc-scrape-examples = true + +[package.metadata.example.bsn_asset_catalog] +name = "BSN Asset Catalog" +description = "Demonstrates loading named material definitions from a BSN asset catalog" +category = "Scene" +wasm = true + +[[example]] +name = "dynamic_bsn" +path = "examples/scene/dynamic_bsn.rs" + # Shaders [[package.metadata.example_category]] name = "Shaders" diff --git a/assets/scenes/example.bsn b/assets/scenes/example.bsn new file mode 100644 index 0000000000000..06601709eb5e9 --- /dev/null +++ b/assets/scenes/example.bsn @@ -0,0 +1,31 @@ +// Example BSN file. + +#Root +bevy_transform::components::transform::Transform +bevy_camera::visibility::Visibility::Visible +bevy_ecs::hierarchy::Children [ + bevy_camera::components::Camera3d + bevy_transform::components::transform::Transform { + translation: glam::Vec3 { x: 0.7, y: 0.7, z: 1.0 }, + rotation: glam::Quat { x: -0.15037778, y: 0.2968788, z: 0.04740241, w: 0.941808 }, + } + bevy_camera::visibility::Visibility::Visible + bevy_light::probe::EnvironmentMapLight { + diffuse_map: "environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2", + specular_map: "environment_maps/pisa_specular_rgb9e5_zstd.ktx2", + intensity: 250.0, + }, + + bevy_transform::components::transform::Transform + // Embed an asset as a handle: + bevy_scene::components::SceneRoot("models/FlightHelmet/FlightHelmet.gltf#Scene0"), + + bevy_light::directional_light::DirectionalLight { + shadow_maps_enabled: true, + } + // A template: + @bevy_light::cascade::CascadeShadowConfigBuilder { + num_cascades: 1, + maximum_distance: 1.6, + } +] diff --git a/assets/scenes/material_catalog.bsn b/assets/scenes/material_catalog.bsn new file mode 100644 index 0000000000000..b97db0f98d672 --- /dev/null +++ b/assets/scenes/material_catalog.bsn @@ -0,0 +1,34 @@ +// Example BSN asset catalog: named materials for reuse across scenes. +// +// Each named entry creates an asset that can be loaded via: +// asset_server.load("scenes/material_catalog.bsn#PolishedMetal") + +bevy_ecs::hierarchy::Children [ + #PolishedMetal + bevy_pbr::pbr_material::StandardMaterial { + metallic: 1.0, + perceptual_roughness: 0.05, + reflectance: 1.0, + }, + + #BrushedMetal + bevy_pbr::pbr_material::StandardMaterial { + metallic: 1.0, + perceptual_roughness: 0.4, + reflectance: 0.8, + }, + + #RoughStone + bevy_pbr::pbr_material::StandardMaterial { + metallic: 0.0, + perceptual_roughness: 0.9, + reflectance: 0.3, + }, + + #Plastic + bevy_pbr::pbr_material::StandardMaterial { + metallic: 0.0, + perceptual_roughness: 0.3, + reflectance: 0.5, + }, +] diff --git a/crates/bevy_asset/src/handle.rs b/crates/bevy_asset/src/handle.rs index 044b33c89245b..6d4b32f7398d7 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -3,9 +3,9 @@ use crate::{ ErasedAssetIndex, ReflectHandle, UntypedAssetId, }; use alloc::sync::Arc; -use bevy_ecs::template::{FromTemplate, SpecializeFromTemplate, Template, TemplateContext}; +use bevy_ecs::{reflect::{ReflectFromTemplate, ReflectTemplate}, template::{FromTemplate, SpecializeFromTemplate, Template, TemplateContext}}; use bevy_platform::collections::Equivalent; -use bevy_reflect::{Reflect, TypePath}; +use bevy_reflect::{FromReflect, Reflect, TypePath, prelude::ReflectDefault}; use core::{ any::TypeId, hash::{Hash, Hasher}, @@ -130,7 +130,7 @@ impl core::fmt::Debug for StrongHandle { /// /// [`Handle::Strong`], via [`StrongHandle`] also provides access to useful [`Asset`] metadata, such as the [`AssetPath`] (if it exists). #[derive(Reflect)] -#[reflect(Debug, Hash, PartialEq, Clone, Handle)] +#[reflect(Debug, Hash, PartialEq, Clone, Handle, FromTemplate)] pub enum Handle { /// A "strong" reference to a live (or loading) [`Asset`]. If a [`Handle`] is [`Handle::Strong`], the [`Asset`] will be kept /// alive until the [`Handle`] is dropped. Strong handles also provide access to additional asset metadata. @@ -208,6 +208,8 @@ impl FromTemplate for Handle { type Template = HandleTemplate; } +#[derive(Reflect)] +#[reflect(Default, Template)] pub enum HandleTemplate { Path(AssetPath<'static>), Handle(Handle), diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index c2d63a44ae740..b3a6c0e620e3c 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -520,6 +520,41 @@ impl<'a> LoadContext<'a> { handle } + /// Add a type-erased [`ErasedLoadedAsset`] as a labeled sub-asset. Used for + /// registering assets created via reflection where the concrete type is not + /// known at compile time. + /// + /// See [`AssetPath`] for more on labeled assets. + pub fn add_loaded_labeled_asset_erased( + &mut self, + label: impl Into>, + loaded_asset: ErasedLoadedAsset, + asset_type_id: TypeId, + ) -> UntypedHandle { + let label = label.into(); + let labeled_path = self.asset_path.clone().with_label(label.clone()); + let handle = self + .asset_server + .get_or_create_path_handle_erased(labeled_path, asset_type_id, None); + let asset = LabeledAsset { + asset: loaded_asset, + handle: handle.clone(), + }; + match self.label_to_asset_index.entry(label) { + Entry::Occupied(entry) => { + let index = *entry.get(); + self.labeled_assets[index] = asset; + } + Entry::Vacant(entry) => { + entry.insert(self.labeled_assets.len()); + self.asset_id_to_asset_index + .insert(handle.id(), self.labeled_assets.len()); + self.labeled_assets.push(asset); + } + } + handle + } + /// Returns `true` if an asset with the label `label` exists in this context. /// /// See [`AssetPath`] for more on labeled assets. diff --git a/crates/bevy_asset/src/reflect.rs b/crates/bevy_asset/src/reflect.rs index b7c569b153ce4..faa6a8a575bc5 100644 --- a/crates/bevy_asset/src/reflect.rs +++ b/crates/bevy_asset/src/reflect.rs @@ -12,6 +12,7 @@ use bevy_reflect::{ }; use crate::{ + loader::{ErasedLoadedAsset, LoadedAsset}, Asset, AssetId, AssetPath, AssetServer, Assets, Handle, InvalidGenerationError, LoadContext, UntypedAssetId, UntypedHandle, }; @@ -39,6 +40,7 @@ pub struct ReflectAsset { len: fn(&World) -> usize, ids: for<'w> fn(&'w World) -> Box + 'w>, remove: fn(&mut World, UntypedAssetId) -> Option>, + into_loaded_asset: fn(&dyn PartialReflect) -> Option, } impl ReflectAsset { @@ -154,6 +156,15 @@ impl ReflectAsset { pub fn ids<'w>(&self, world: &'w World) -> impl Iterator + 'w { (self.ids)(world) } + + /// Convert a reflected asset value into an [`ErasedLoadedAsset`] suitable + /// for registering as a labeled sub-asset via [`LoadContext`]. + pub fn into_loaded_asset( + &self, + value: &dyn PartialReflect, + ) -> Option { + (self.into_loaded_asset)(value) + } } impl FromType for ReflectAsset { @@ -199,6 +210,10 @@ impl FromType for ReflectAsset { let value = assets.remove(asset_id.typed_debug_checked()); value.map(|value| Box::new(value) as Box) }, + into_loaded_asset: |value| { + let asset: A = FromReflect::from_reflect(value)?; + Some(LoadedAsset::from(asset).into()) + }, } } } diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index bdbeb8fef6469..cc4d7dadc7140 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -76,7 +76,6 @@ std = [ "arrayvec/std", "log/std", "bevy_platform/std", - "downcast-rs/std", ] ## `critical-section` provides the building blocks for synchronization primitives @@ -127,7 +126,6 @@ log = { version = "0.4", default-features = false } bumpalo = "3" subsecond = { version = "0.7.0-rc.0", optional = true } slotmap = { version = "1.0.7", default-features = false } -downcast-rs = { version = "2", default-features = false } concurrent-queue = { version = "2.5.0", default-features = false } [target.'cfg(not(all(target_has_atomic = "8", target_has_atomic = "16", target_has_atomic = "32", target_has_atomic = "64", target_has_atomic = "ptr")))'.dependencies] diff --git a/crates/bevy_ecs/macros/src/template.rs b/crates/bevy_ecs/macros/src/template.rs index 91b3b82d7210c..3d9cefe982a8d 100644 --- a/crates/bevy_ecs/macros/src/template.rs +++ b/crates/bevy_ecs/macros/src/template.rs @@ -8,6 +8,7 @@ use syn::{ const TEMPLATE_DEFAULT_ATTRIBUTE: &str = "default"; const TEMPLATE_ATTRIBUTE: &str = "template"; +const TEMPLATE_REFLECT: &str = "reflect"; pub(crate) fn derive_from_template(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); @@ -21,6 +22,27 @@ pub(crate) fn derive_from_template(input: TokenStream) -> TokenStream { let is_pub = matches!(ast.vis, syn::Visibility::Public(_)); let maybe_pub = if is_pub { quote!(pub) } else { quote!() }; + let should_make_template_reflectable = ast + .attrs + .iter() + .filter(|attr| attr.path().is_ident(&TEMPLATE_ATTRIBUTE)) + .any(|template_attr| { + let mut found = false; + let _ = template_attr.parse_nested_meta(|meta| { + found = found || meta.path.is_ident(TEMPLATE_REFLECT); + Ok(()) + }); + found + }); + let maybe_reflect = if should_make_template_reflectable { + quote! { + #[derive(Reflect)] + #[reflect(Default, Template)] + } + } else { + quote! {} + }; + let template = match &ast.data { Data::Struct(data_struct) => { let result = match struct_impl(&data_struct.fields, &bevy_ecs, false) { @@ -38,6 +60,7 @@ pub(crate) fn derive_from_template(input: TokenStream) -> TokenStream { Fields::Named(_) => { quote! { #[allow(missing_docs)] + #maybe_reflect #maybe_pub struct #template_ident #impl_generics #where_clause { #(#template_fields,)* } @@ -69,6 +92,7 @@ pub(crate) fn derive_from_template(input: TokenStream) -> TokenStream { Fields::Unnamed(_) => { quote! { #[allow(missing_docs)] + #maybe_reflect #maybe_pub struct #template_ident #impl_generics ( #(#template_fields,)* ) #where_clause; @@ -100,6 +124,7 @@ pub(crate) fn derive_from_template(input: TokenStream) -> TokenStream { Fields::Unit => { quote! { #[allow(missing_docs)] + #maybe_reflect #maybe_pub struct #template_ident; impl #impl_generics #bevy_ecs::template::Template for #template_ident #type_generics #where_clause { @@ -269,6 +294,7 @@ pub(crate) fn derive_from_template(input: TokenStream) -> TokenStream { quote! { #[allow(missing_docs)] + #maybe_reflect #maybe_pub enum #template_ident #type_generics #where_clause { #(#variant_definitions,)* } diff --git a/crates/bevy_ecs/src/reflect/mod.rs b/crates/bevy_ecs/src/reflect/mod.rs index 1ede8a081e740..3ce20251e1037 100644 --- a/crates/bevy_ecs/src/reflect/mod.rs +++ b/crates/bevy_ecs/src/reflect/mod.rs @@ -19,6 +19,7 @@ mod from_world; mod map_entities; mod message; mod resource; +mod template; use bevy_utils::prelude::DebugName; pub use bundle::{ReflectBundle, ReflectBundleFns}; @@ -29,6 +30,7 @@ pub use from_world::{ReflectFromWorld, ReflectFromWorldFns}; pub use map_entities::ReflectMapEntities; pub use message::{ReflectMessage, ReflectMessageFns}; pub use resource::ReflectResource; +pub use template::{ReflectFromTemplate, ReflectTemplate}; /// A [`Resource`] storing [`TypeRegistry`] for /// type registrations relevant to a whole app. diff --git a/crates/bevy_ecs/src/reflect/template.rs b/crates/bevy_ecs/src/reflect/template.rs new file mode 100644 index 0000000000000..6ff8fb70f1722 --- /dev/null +++ b/crates/bevy_ecs/src/reflect/template.rs @@ -0,0 +1,60 @@ +//! Definitions for `FromTemplate` and `Template` reflection. + +use alloc::boxed::Box; +use core::any::TypeId; + +use bevy_reflect::{FromType, Reflect}; +use derive_more::{Deref, DerefMut}; + +use crate::{ + error::BevyError, + prelude::{FromTemplate, Template}, + template::TemplateContext, +}; + +#[derive(Clone, Deref, DerefMut)] +pub struct ReflectFromTemplate(pub ReflectFromTemplateData); + +#[derive(Clone, Deref, DerefMut)] +pub struct ReflectTemplate(pub ReflectTemplateData); + +#[derive(Clone)] +pub struct ReflectFromTemplateData { + pub template_type_id: TypeId, +} + +#[derive(Clone)] +pub struct ReflectTemplateData { + pub build_template: + fn(&dyn Reflect, &mut TemplateContext) -> Result, BevyError>, +} + +impl FromType for ReflectFromTemplate +where + T: FromTemplate, + T::Template: 'static, + ::Output: Reflect, +{ + fn from_type() -> Self { + ReflectFromTemplate(ReflectFromTemplateData { + template_type_id: TypeId::of::(), + }) + } +} + +impl FromType for ReflectTemplate +where + T: Template + 'static, + ::Output: Reflect, +{ + fn from_type() -> Self { + ReflectTemplate(ReflectTemplateData { + build_template: |this, context| { + let Some(this) = this.downcast_ref::() else { + return Err("Unexpected `build_template` receiver type".into()); + }; + Ok(Box::new(::build_template(this, context)?)) + }, + }) + } +} diff --git a/crates/bevy_ecs/src/template.rs b/crates/bevy_ecs/src/template.rs index 31a6d77472d50..ee4407f2ed9f6 100644 --- a/crates/bevy_ecs/src/template.rs +++ b/crates/bevy_ecs/src/template.rs @@ -1,6 +1,7 @@ //! Functionality that relates to the [`Template`] trait. pub use bevy_ecs_macros::FromTemplate; +use bevy_reflect::PartialReflect; use crate::{ bundle::Bundle, @@ -10,7 +11,9 @@ use crate::{ world::{EntityWorldMut, Mut, World}, }; use alloc::{boxed::Box, vec, vec::Vec}; -use downcast_rs::{impl_downcast, Downcast}; +use bevy_platform::collections::hash_map::Entry; +use bevy_utils::TypeIdMap; +use core::any::{Any, TypeId}; use variadics_please::all_tuples; /// A [`Template`] is something that, given a spawn context (target [`Entity`], [`World`], etc), can produce a [`Template::Output`]. @@ -388,15 +391,17 @@ impl FromTemplate for Entity { } /// A type-erased, object-safe, downcastable version of [`Template`]. -pub trait ErasedTemplate: Downcast + Send + Sync { +pub trait ErasedTemplate: Send + Sync { /// Applies this template to the given `entity`. fn apply(&self, context: &mut TemplateContext) -> Result<(), BevyError>; /// Clones this template. See [`Clone`]. fn clone_template(&self) -> Box; -} -impl_downcast!(ErasedTemplate); + fn as_any_mut(&mut self) -> &mut dyn Any; + + fn try_as_partial_reflect_mut(&mut self) -> Option<&mut dyn PartialReflect>; +} impl + Send + Sync + 'static> ErasedTemplate for T { fn apply(&self, context: &mut TemplateContext) -> Result<(), BevyError> { @@ -408,6 +413,14 @@ impl + Send + Sync + 'static> ErasedTemplate for T { fn clone_template(&self) -> Box { Box::new(Template::clone_template(self)) } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn try_as_partial_reflect_mut(&mut self) -> Option<&mut dyn PartialReflect> { + None + } } /// A [`Template`] driven by a function that returns an output. This is used to create "free floating" templates without diff --git a/crates/bevy_light/src/cascade.rs b/crates/bevy_light/src/cascade.rs index e2c4e787cadfd..3425b1f7b391f 100644 --- a/crates/bevy_light/src/cascade.rs +++ b/crates/bevy_light/src/cascade.rs @@ -1,7 +1,9 @@ //! Provides shadow cascade configuration and construction helpers. use bevy_camera::{Camera, Projection}; -use bevy_ecs::{entity::EntityHashMap, prelude::*}; +use bevy_ecs::{ + entity::EntityHashMap, prelude::*, reflect::ReflectTemplate, template::TemplateContext, +}; use bevy_math::{ops, Mat4, Vec3A, Vec4}; use bevy_reflect::prelude::*; use bevy_transform::components::GlobalTransform; @@ -56,6 +58,8 @@ fn calculate_cascade_bounds( } /// Builder for [`CascadeShadowConfig`]. +#[derive(Reflect)] +#[reflect(Default, Template)] pub struct CascadeShadowConfigBuilder { /// The number of shadow cascades. /// More cascades increases shadow quality by mitigating perspective aliasing - a phenomenon where areas @@ -163,6 +167,24 @@ impl From for CascadeShadowConfig { } } +impl Template for CascadeShadowConfigBuilder { + type Output = CascadeShadowConfig; + + fn build_template(&self, _: &mut TemplateContext) -> Result { + Ok(self.build()) + } + + fn clone_template(&self) -> Self { + Self { + num_cascades: self.num_cascades, + minimum_distance: self.minimum_distance, + maximum_distance: self.maximum_distance, + first_cascade_far_bound: self.first_cascade_far_bound, + overlap_proportion: self.overlap_proportion, + } + } +} + /// A [`DirectionalLight`]'s per-view list of [`Cascade`]s. #[derive(Component, Clone, Debug, Default, Reflect)] #[reflect(Component, Debug, Default, Clone)] diff --git a/crates/bevy_light/src/probe.rs b/crates/bevy_light/src/probe.rs index a1b22d998b182..cab4251782f4d 100644 --- a/crates/bevy_light/src/probe.rs +++ b/crates/bevy_light/src/probe.rs @@ -1,7 +1,10 @@ use bevy_asset::{Assets, Handle, RenderAssetUsages}; use bevy_camera::visibility::{self, ViewVisibility, Visibility, VisibilityClass}; use bevy_color::{Color, ColorToComponents, Srgba}; -use bevy_ecs::prelude::*; +use bevy_ecs::{ + prelude::*, + reflect::{ReflectFromTemplate, ReflectTemplate}, +}; use bevy_image::Image; use bevy_math::{Quat, UVec2, Vec3}; use bevy_reflect::prelude::*; @@ -101,8 +104,9 @@ impl LightProbe { /// area in space. /// /// See `bevy_pbr::environment_map` for detailed information. -#[derive(Clone, Component, Reflect)] -#[reflect(Component, Default, Clone)] +#[derive(Clone, Component, Reflect, FromTemplate)] +#[reflect(Component, Clone, FromTemplate)] +#[template(reflect)] pub struct EnvironmentMapLight { /// The blurry image that represents diffuse radiance surrounding a region. pub diffuse_map: Handle, @@ -159,7 +163,7 @@ impl EnvironmentMapLight { Self { diffuse_map: handle.clone(), specular_map: handle, - ..Default::default() + ..EnvironmentMapLight::default() } } @@ -200,10 +204,8 @@ impl EnvironmentMapLight { ) } } -} -impl Default for EnvironmentMapLight { - fn default() -> Self { + pub fn default() -> Self { EnvironmentMapLight { diffuse_map: Handle::default(), specular_map: Handle::default(), diff --git a/crates/bevy_scene/src/components.rs b/crates/bevy_scene/src/components.rs index e6c556e6afcfa..4e5e5013b323a 100644 --- a/crates/bevy_scene/src/components.rs +++ b/crates/bevy_scene/src/components.rs @@ -1,6 +1,10 @@ use bevy_asset::{AsAssetId, AssetId, Handle}; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::{component::Component, prelude::ReflectComponent, template::FromTemplate}; +use bevy_ecs::{ + component::Component, + prelude::{FromTemplate, ReflectComponent}, + reflect::{ReflectFromTemplate, ReflectTemplate}, +}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_transform::components::Transform; use derive_more::derive::From; @@ -11,10 +15,9 @@ use crate::{DynamicScene, Scene}; /// Adding this component will spawn the scene as a child of that entity. /// Once it's spawned, the entity will have a [`SceneInstance`](crate::SceneInstance) component. -#[derive( - Component, FromTemplate, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From, -)] -#[reflect(Component, Default, Debug, PartialEq, Clone)] +#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From, FromTemplate)] +#[reflect(Component, Default, Debug, PartialEq, Clone, FromTemplate)] +#[template(reflect)] #[require(Transform)] #[require(Visibility)] pub struct SceneRoot(pub Handle); diff --git a/crates/bevy_scene2/Cargo.toml b/crates/bevy_scene2/Cargo.toml index 1e78de1e628e0..1bc0ef644537e 100644 --- a/crates/bevy_scene2/Cargo.toml +++ b/crates/bevy_scene2/Cargo.toml @@ -20,8 +20,14 @@ bevy_platform = { path = "../bevy_platform", version = "0.19.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.19.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.19.0-dev" } +lalrpop-util = "0.23" +nom = "8" thiserror = { version = "2", default-features = false } tracing = { version = "0.1", default-features = false, features = ["std"] } variadics_please = "1.0" + +[build-dependencies] +lalrpop = "0.23" + [lints] workspace = true diff --git a/crates/bevy_scene2/build.rs b/crates/bevy_scene2/build.rs new file mode 100644 index 0000000000000..7e68f910b319b --- /dev/null +++ b/crates/bevy_scene2/build.rs @@ -0,0 +1,3 @@ +fn main() { + lalrpop::process_src().unwrap(); +} diff --git a/crates/bevy_scene2/src/bsn_asset_catalog.rs b/crates/bevy_scene2/src/bsn_asset_catalog.rs new file mode 100644 index 0000000000000..4a4bb40850dc8 --- /dev/null +++ b/crates/bevy_scene2/src/bsn_asset_catalog.rs @@ -0,0 +1,404 @@ +//! BSN asset catalog: load and save named asset definitions in `.bsn` format. +//! +//! **Loading**: [`load_bsn_assets`] parses BSN text containing named asset +//! definitions and inserts them into `Assets` stores via reflection. +//! +//! **Saving**: [`serialize_assets_to_bsn`] reflects named assets from the world +//! and emits BSN text with default-diffing (only non-default fields are written). + +use core::any::TypeId; +use core::fmt::Write; + +use bevy_asset::{AssetServer, ReflectHandle, UntypedAssetId, UntypedHandle}; +use bevy_ecs::{prelude::*, reflect::AppTypeRegistry}; +use bevy_reflect::{prelude::ReflectDefault, PartialReflect, ReflectMut, ReflectRef, TypeRegistry}; + +use crate::dynamic_bsn::{BsnAst, BsnExpr, BsnNameStore, BsnPatch, BsnPatches}; +use crate::dynamic_bsn_grammar::TopLevelPatchesParser; +use crate::dynamic_bsn_lexer::Lexer; + +/// A named asset entry produced by [`load_bsn_assets`]. +pub struct CatalogEntry { + /// The `#Name` from the BSN catalog. + pub name: String, + /// Handle to the created asset in the `Assets` store. + pub handle: UntypedHandle, +} + +/// A named asset reference for serialization by [`serialize_assets_to_bsn`]. +pub struct CatalogAssetRef { + /// Display name for the asset in the catalog (becomes `#Name` in BSN). + pub name: String, + /// The concrete asset type (e.g., `TypeId::of::()`). + pub type_id: TypeId, + /// The asset's ID in its `Assets` store. + pub asset_id: UntypedAssetId, +} + +// --------------------------------------------------------------------------- +// Loading +// --------------------------------------------------------------------------- + +/// Parse BSN text containing named asset definitions and insert them into +/// the corresponding `Assets` stores via reflection. +pub fn load_bsn_assets( + world: &mut World, + bsn_text: &str, +) -> Result, String> { + let mut parse_world = World::new(); + parse_world.init_resource::(); + let ast = core::cell::RefCell::new(BsnAst(parse_world)); + let lexer = Lexer::new(bsn_text); + let patches_id = TopLevelPatchesParser::new() + .parse(&ast, lexer) + .map_err(|e| format!("BSN asset parse error: {e:?}"))?; + let ast = ast.into_inner(); + + let entries = unwrap_children_wrapper(&ast, patches_id)?; + + let registry = world.resource::().clone(); + let reg = registry.read(); + + let mut results = Vec::new(); + for entry_id in entries { + let Some(patches) = ast.0.get::(entry_id) else { continue }; + + let mut name = None; + let mut handle = None; + + for &pid in &patches.0 { + let Some(patch) = ast.0.get::(pid) else { continue }; + match patch { + BsnPatch::Name(n, _) => name = Some(n.clone()), + BsnPatch::Struct(s) => { + handle = create_asset_from_struct(world, &s.0.as_path(), &s.1, &ast, ®); + } + BsnPatch::Var(v) => { + handle = create_asset_from_struct(world, &v.0.as_path(), &[], &ast, ®); + } + _ => {} + } + } + + if let (Some(name), Some(handle)) = (name, handle) { + results.push(CatalogEntry { name, handle }); + } + } + + Ok(results) +} + +/// If the top-level is a single `Children` relation, unwrap to get the entries inside. +fn unwrap_children_wrapper(ast: &BsnAst, patches_id: Entity) -> Result, String> { + let patches = ast + .0 + .get::(patches_id) + .ok_or("No top-level patches found")?; + if patches.0.len() == 1 { + if let Some(BsnPatch::Relation(relation)) = ast.0.get::(patches.0[0]) { + return Ok(relation.1.clone()); + } + } + Ok(vec![patches_id]) +} + +/// Create an asset instance from a BSN struct definition via reflection. +fn create_asset_from_struct( + world: &mut World, + type_path: &str, + fields: &[crate::dynamic_bsn::BsnField], + ast: &BsnAst, + registry: &TypeRegistry, +) -> Option { + let registration = registry.get_with_type_path(type_path)?; + let reflect_default = registration.data::()?; + let mut value = reflect_default.default(); + + if let Ok(struct_info) = registration.type_info().as_struct() { + if let ReflectMut::Struct(s) = value.reflect_mut() { + for field in fields { + let Some(fi) = struct_info.field(&field.0) else { continue }; + let Some(expr) = ast.0.get::(field.1) else { continue }; + apply_bsn_expr(s, &field.0, expr, fi.ty().id(), registry, ast); + } + } + } + + let reflect_asset = registration.data::()?; + Some(reflect_asset.add(world, value.as_partial_reflect())) +} + +/// Apply a BSN expression value to a struct field via reflection. +fn apply_bsn_expr( + target: &mut dyn bevy_reflect::structs::Struct, + field_name: &str, + expr: &BsnExpr, + expected_type: TypeId, + registry: &TypeRegistry, + ast: &BsnAst, +) { + let Some(field) = target.field_mut(field_name) else { return }; + + match expr { + BsnExpr::FloatLit(f) => { + if expected_type == TypeId::of::() { + field.apply(&(*f as f32)); + } else if expected_type == TypeId::of::() { + field.apply(f); + } + } + BsnExpr::IntLit(i) => { + macro_rules! try_int { + ($($t:ty),*) => { + $(if expected_type == TypeId::of::<$t>() { field.apply(&(*i as $t)); return; })* + }; + } + try_int!(i8, u8, i16, u16, i32, u32, i64, u64, usize, isize); + } + BsnExpr::BoolLit(b) => field.apply(b), + BsnExpr::StringLit(s) => { + if expected_type == TypeId::of::() { + field.apply(s); + } + } + BsnExpr::Struct(bsn_struct) => { + let type_path = bsn_struct.0.as_path(); + let Some(reg) = registry.get_with_type_path(&type_path) else { return }; + let Ok(si) = reg.type_info().as_struct() else { return }; + let ReflectMut::Struct(s) = field.reflect_mut() else { return }; + for f in &bsn_struct.1 { + let Some(fi) = si.field(&f.0) else { continue }; + let Some(e) = ast.0.get::(f.1) else { continue }; + apply_bsn_expr(s, &f.0, e, fi.ty().id(), registry, ast); + } + } + _ => {} + } +} + +// --------------------------------------------------------------------------- +// Serialization +// --------------------------------------------------------------------------- + +/// Serialize named assets to a BSN catalog string. +/// +/// Each entry is reflected from its `Assets` store, compared against its +/// default, and emitted with only non-default fields. +pub fn serialize_assets_to_bsn(world: &World, assets: &[CatalogAssetRef]) -> String { + if assets.is_empty() { + return String::new(); + } + + let registry = world.resource::().clone(); + let reg = registry.read(); + let asset_server = world.get_resource::(); + + let mut entries: Vec<(String, String)> = Vec::new(); + + for asset_ref in assets { + let Some(registration) = reg.get(asset_ref.type_id) else { continue }; + let Some(reflect_asset) = registration.data::() else { continue }; + let Some(asset_data) = reflect_asset.get(world, asset_ref.asset_id) else { continue }; + + let type_path = registration.type_info().type_path_table().path(); + let default_value = registration.data::().map(|rd| rd.default()); + + let mut entry = String::new(); + write_name(&asset_ref.name, 1, &mut entry); + + if let ReflectRef::Struct(s) = asset_data.reflect_ref() { + let default_struct = default_value.as_ref().and_then(|d| match d.reflect_ref() { + ReflectRef::Struct(ds) => Some(ds), + _ => None, + }); + let fields = diff_struct_fields(s, default_struct, ®, asset_server); + write_struct(type_path, &fields, 1, &mut entry); + } else { + write_indent(&mut entry, 1, &format!("{type_path}\n")); + } + + entries.push((asset_ref.name.clone(), entry)); + } + + entries.sort_by(|a, b| a.0.cmp(&b.0)); + + let mut out = String::from("bevy_ecs::hierarchy::Children [\n"); + for (i, (_, entry)) in entries.iter().enumerate() { + out.push_str(entry); + if i + 1 < entries.len() { + out.push_str(" ,\n"); + } + } + out.push_str("]\n"); + out +} + +/// Collect struct fields that differ from defaults. +fn diff_struct_fields( + s: &dyn bevy_reflect::structs::Struct, + default: Option<&dyn bevy_reflect::structs::Struct>, + registry: &TypeRegistry, + asset_server: Option<&AssetServer>, +) -> Vec<(String, String)> { + let mut fields = Vec::new(); + for i in 0..s.field_len() { + let name = s.name_at(i).unwrap(); + let value = s.field_at(i).unwrap(); + + // Skip fields that match the default + if let Some(ds) = default { + if let Some(df) = ds.field(name) { + if value.reflect_partial_eq(df).unwrap_or(false) { + continue; + } + } + } + + // Handle -> asset path string + if let Some(path) = resolve_handle_path(value, registry, asset_server) { + fields.push((name.to_string(), format!("\"{path}\""))); + continue; + } + + // Option> -> unwrap Some, skip None + if let ReflectRef::Enum(e) = value.reflect_ref() { + if e.variant_name() == "None" { + continue; + } + if e.variant_name() == "Some" { + if let Some(inner) = e.field_at(0) { + if let Some(path) = resolve_handle_path(inner, registry, asset_server) { + fields.push((name.to_string(), format!("\"{path}\""))); + continue; + } + } + } + } + + // Skip generic types the BSN parser can't round-trip + if let Some(ti) = value.get_represented_type_info() { + if ti.type_path().contains('<') { + continue; + } + } + + let mut val = String::new(); + write_value(value, registry, asset_server, &mut val); + fields.push((name.to_string(), val)); + } + fields +} + +/// Try to resolve a reflected value as a Handle and return its asset path. +fn resolve_handle_path( + value: &dyn PartialReflect, + registry: &TypeRegistry, + asset_server: Option<&AssetServer>, +) -> Option { + let asset_server = asset_server?; + let concrete = value.try_as_reflect()?; + let type_id = concrete.reflect_type_info().type_id(); + let reflect_handle = registry.get_type_data::(type_id)?; + let handle = reflect_handle.downcast_handle_untyped(concrete.as_any())?; + let path = asset_server.get_path(handle.id())?; + Some(path.path().to_string_lossy().into_owned()) +} + +/// Write a single reflected value as inline BSN text. +fn write_value( + value: &dyn PartialReflect, + registry: &TypeRegistry, + asset_server: Option<&AssetServer>, + out: &mut String, +) { + if let Some(v) = value.try_downcast_ref::() { + return write_float(*v, out); + } + if let Some(v) = value.try_downcast_ref::() { + return write_float(*v as f32, out); + } + if let Some(v) = value.try_downcast_ref::() { + return write!(out, "{v}").unwrap(); + } + if let Some(v) = value.try_downcast_ref::() { + return write!(out, "\"{}\"", v.replace('\\', "\\\\").replace('"', "\\\"")).unwrap(); + } + macro_rules! try_int { + ($($t:ty),*) => { + $(if let Some(v) = value.try_downcast_ref::<$t>() { return write!(out, "{v}").unwrap(); })* + }; + } + try_int!(i8, u8, i16, u16, i32, u32, i64, u64, isize, usize); + + if let Some(path) = resolve_handle_path(value, registry, asset_server) { + return write!(out, "\"{path}\"").unwrap(); + } + + let tp = value + .get_represented_type_info() + .map(|i| i.type_path()) + .unwrap_or("unknown"); + + match value.reflect_ref() { + ReflectRef::Struct(s) if s.field_len() > 0 => { + write!(out, "{tp} {{ ").unwrap(); + for i in 0..s.field_len() { + if i > 0 { write!(out, ", ").unwrap(); } + write!(out, "{}: ", s.name_at(i).unwrap()).unwrap(); + write_value(s.field_at(i).unwrap(), registry, asset_server, out); + } + write!(out, " }}").unwrap(); + } + ReflectRef::Struct(_) => write!(out, "{tp}").unwrap(), + ReflectRef::TupleStruct(ts) => { + write!(out, "{tp}(").unwrap(); + for i in 0..ts.field_len() { + if i > 0 { write!(out, ", ").unwrap(); } + write_value(ts.field(i).unwrap(), registry, asset_server, out); + } + write!(out, ")").unwrap(); + } + ReflectRef::Enum(e) => write!(out, "{tp}::{}", e.variant_name()).unwrap(), + _ => write!(out, "\"\"").unwrap(), + } +} + +// --------------------------------------------------------------------------- +// BSN formatting helpers +// --------------------------------------------------------------------------- + +fn write_struct(type_path: &str, fields: &[(String, String)], indent: usize, out: &mut String) { + if fields.is_empty() { + write_indent(out, indent, &format!("{type_path}\n")); + } else { + write_indent(out, indent, &format!("{type_path} {{\n")); + for (name, val) in fields { + write_indent(out, indent + 1, &format!("{name}: {val},\n")); + } + write_indent(out, indent, "}\n"); + } +} + +fn write_name(name: &str, indent: usize, out: &mut String) { + if !name.is_empty() && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { + write_indent(out, indent, &format!("#{name}\n")); + } else { + let escaped = name.replace('\\', "\\\\").replace('"', "\\\""); + write_indent(out, indent, &format!("#\"{escaped}\"\n")); + } +} + +fn write_indent(out: &mut String, indent: usize, text: &str) { + for _ in 0..indent { + out.push_str(" "); + } + out.push_str(text); +} + +fn write_float(f: f32, out: &mut String) { + if f.fract() == 0.0 { + write!(out, "{f:.1}").unwrap(); + } else { + write!(out, "{f}").unwrap(); + } +} diff --git a/crates/bevy_scene2/src/dynamic_bsn.rs b/crates/bevy_scene2/src/dynamic_bsn.rs new file mode 100644 index 0000000000000..b14f4a327cbf7 --- /dev/null +++ b/crates/bevy_scene2/src/dynamic_bsn.rs @@ -0,0 +1,1097 @@ +//! BSN assets loaded from `.bsn` files. + +use bevy_asset::{io::Reader, AssetLoader, AssetPath, LoadContext}; +use bevy_ecs::{ + entity::Entity, + error::{BevyError, Result as EcsResult}, + hierarchy::ChildOf, + name::Name, + prelude::{Component, Resource}, + reflect::{AppTypeRegistry, ReflectFromTemplate, ReflectTemplate}, + template::{ErasedTemplate, TemplateContext}, + world::{FromWorld, World}, +}; +use bevy_log::error; +use bevy_platform::collections::HashMap; +use bevy_reflect::{ + enums::{DynamicEnum, DynamicVariant, StructVariantInfo, VariantInfoError}, + list::DynamicList, + prelude::ReflectDefault, + structs::{DynamicStruct, StructInfo}, + tuple_struct::DynamicTupleStruct, + NamedField, PartialReflect, Reflect, ReflectMut, TypePath, TypeRegistration, TypeRegistry, +}; +use core::{ + any::{Any, TypeId}, + cell::RefCell, + fmt::Write, + mem, + str::Utf8Error, +}; +use std::io::Error as IoError; +use thiserror::Error; + +use crate::{ + dynamic_bsn_grammar::TopLevelPatchesParser, dynamic_bsn_lexer::Lexer, InheritSceneAsset, + NameEntityReference, RelatedResolvedScenes, ResolveContext, ResolveSceneError, ResolvedScene, + Scene, SceneDependencies, ScenePatch, SceneScope, +}; + +#[derive(Default)] +pub struct BsnAst(pub World); + +#[derive(Resource, Default)] +pub struct BsnNameStore { + pub name_indices: HashMap, + pub next_name_index: usize, +} + +#[derive(Component)] +pub struct BsnPatches(pub Vec); + +#[derive(Component)] +pub enum BsnPatch { + Name(String, usize), + Base(String), + Var(BsnVar), + Struct(BsnStruct), + NamedTuple(BsnNamedTuple), + Relation(BsnRelation), +} + +#[derive(Clone)] +pub struct BsnVar(pub BsnSymbol, pub bool); + +#[derive(Clone)] +pub struct BsnSymbol(pub Vec, pub String); + +pub struct BsnStruct(pub BsnSymbol, pub Vec, pub bool); + +pub struct BsnField(pub String, pub Entity); + +pub struct BsnNamedTuple(pub BsnSymbol, pub Vec, pub bool); + +pub struct BsnRelation(pub BsnSymbol, pub Vec); + +#[derive(Component)] +pub enum BsnExpr { + Var(BsnVar), + Struct(BsnStruct), + NamedTuple(BsnNamedTuple), + StringLit(String), + FloatLit(f64), + BoolLit(bool), + IntLit(i128), + List(Vec), +} + +impl BsnSymbol { + pub fn from_ident(ident: String) -> BsnSymbol { + BsnSymbol(vec![], ident) + } + + pub fn append(mut self, ident: String) -> BsnSymbol { + self.0.push(mem::replace(&mut self.1, ident)); + self + } +} + +#[derive(TypePath)] +pub struct DynamicBsnLoader { + type_registry: AppTypeRegistry, +} + +impl FromWorld for DynamicBsnLoader { + fn from_world(world: &mut World) -> Self { + DynamicBsnLoader { + type_registry: world.resource::().clone(), + } + } +} + +// TODO: Report multiple errors +#[derive(Error, Debug)] +pub enum DynamicBsnLoaderError { + #[error("I/O error: {0}")] + Io(#[from] IoError), + #[error("UTF-8 error: {0}")] + Utf8(#[from] Utf8Error), + #[error("parse error: {0}")] + Parse(String), + #[error("no such AST node")] + NoSuchAstNode, + #[error("only `Children` relations supported")] + OnlyChildRelationsSupported, + #[error("type doesn't implement `Default`: {0}")] + TypeDoesntImplementDefault(String), + #[error("type isn't a tuple structure")] + TypeNotNamedTuple, + #[error("type isn't a structure")] + TypeNotStruct, + #[error("variant isn't a tuple variant: {0}")] + VariantNotTuple(#[from] VariantInfoError), + #[error("structure doesn't have a field named `{0}`")] + StructDoesntHaveField(String), + #[error("unknown type: `{0}`")] + UnknownType(String), + #[error("type mismatch")] + TypeMismatch, + #[error("type mismatch, expected `f32` or `f64`")] + FloatLitTypeMismatch, + #[error( + "type mismatch, expected `i8`, `u8`, `i16`, `u16`, `i32`, `u32`, `i64`, `u64`, \ + `isize`, or `usize`" + )] + IntLitTypeMismatch, +} + +impl AssetLoader for DynamicBsnLoader { + type Asset = ScenePatch; + + type Settings = (); + + type Error = DynamicBsnLoaderError; + + fn extensions(&self) -> &[&str] { + &["bsn"] + } + + async fn load( + &self, + reader: &mut dyn Reader, + _settings: &Self::Settings, + load_context: &mut LoadContext<'_>, + ) -> Result { + let mut buffer = vec![]; + reader.read_to_end(&mut buffer).await?; + let input = str::from_utf8(&buffer)?; + + let mut world = World::new(); + world.init_resource::(); + let ast = RefCell::new(BsnAst(world)); + + let lexer = Lexer::new(input); + let patches_id = match TopLevelPatchesParser::new().parse(&ast, lexer) { + Ok(patches_id) => patches_id, + Err(err) => { + return Err(DynamicBsnLoaderError::Parse(format!("{:?}", err))); + } + }; + + let ast = ast.into_inner(); + + // Register named asset entries as labeled sub-assets (e.g., materials) + self.register_labeled_assets(&ast, patches_id, load_context); + + let patch = ast.convert_bsn_patches_to_patch(patches_id, &self.type_registry)?; + + Ok(ScenePatch { + scene: Box::new(SceneScope(patch.scene)), + dependencies: patch.dependencies, + resolved: None, + }) + } +} + +impl DynamicBsnLoader { + /// Scan the parsed AST for named entries with asset types and register them + /// as labeled sub-assets so they're resolvable via + /// `asset_server.load("file.bsn#name")`. + fn register_labeled_assets( + &self, + ast: &BsnAst, + patches_id: Entity, + load_context: &mut LoadContext<'_>, + ) { + let Some(patches) = ast.0.get::(patches_id) else { + return; + }; + + // Unwrap Children wrapper if present + let entries: Vec = if patches.0.len() == 1 { + if let Some(BsnPatch::Relation(relation)) = ast.0.get::(patches.0[0]) { + relation.1.clone() + } else { + vec![patches_id] + } + } else { + vec![patches_id] + }; + + let type_registry = self.type_registry.read(); + + for entry_id in entries { + let Some(entry_patches) = ast.0.get::(entry_id) else { + continue; + }; + + let mut name: Option = None; + let mut struct_patch: Option<&BsnStruct> = None; + + for &pid in &entry_patches.0 { + let Some(patch) = ast.0.get::(pid) else { + continue; + }; + match patch { + BsnPatch::Name(n, _) => name = Some(n.clone()), + BsnPatch::Struct(s) => struct_patch = Some(s), + _ => {} + } + } + + let (Some(name), Some(bsn_struct)) = (name, struct_patch) else { + continue; + }; + + let type_path = bsn_struct.0.as_path(); + let Some(registration) = type_registry.get_with_type_path(&type_path) else { + continue; + }; + let Some(reflect_asset) = + registration.data::() + else { + continue; + }; + let Some(reflect_default) = + registration.data::() + else { + continue; + }; + + let mut value = reflect_default.default(); + if let bevy_reflect::ReflectMut::Struct(s) = value.reflect_mut() { + if let Ok(struct_info) = registration.type_info().as_struct() { + for field in &bsn_struct.1 { + if let Some(field_info) = struct_info.field(&field.0) { + if let Ok(reflected) = ast.convert_bsn_expr_to_reflect( + field.1, + &self.type_registry, + field_info.ty().id(), + ) { + if let Some(target) = s.field_mut(&field.0) { + target.apply(&*reflected); + } + } + } + } + } + } + + if let Some(erased) = reflect_asset.into_loaded_asset(value.as_partial_reflect()) { + load_context.add_loaded_labeled_asset_erased( + name, + erased, + registration.type_id(), + ); + } + } + } +} + +impl BsnAst { + fn convert_bsn_patches_to_patch( + &self, + patches_id: Entity, + app_type_registry: &AppTypeRegistry, + ) -> Result { + let Some(patches) = self.0.get::(patches_id) else { + return Err(DynamicBsnLoaderError::NoSuchAstNode); + }; + let mut scene_patches: Vec<_> = patches + .0 + .iter() + .map(|patch_id| self.convert_bsn_patch_to_patch(*patch_id, app_type_registry)) + .collect::, _>>()?; + let dependencies: Vec<_> = scene_patches + .iter_mut() + .flat_map(|scene_patch| mem::take(&mut scene_patch.dependencies)) + .collect(); + Ok(ScenePatch { + scene: Box::new(MultiPatch( + scene_patches + .into_iter() + .map(|scene_patch| scene_patch.scene) + .collect(), + )), + dependencies, + resolved: None, + }) + } + + fn convert_bsn_patch_to_patch( + &self, + patch_id: Entity, + app_type_registry: &AppTypeRegistry, + ) -> Result { + let Some(patch) = self.0.get::(patch_id) else { + return Err(DynamicBsnLoaderError::NoSuchAstNode); + }; + + let patch = match *patch { + BsnPatch::Name(ref name, index) => Box::new(NameEntityReference { + name: Name(name.clone().into()), + index, + }) as Box, + + BsnPatch::Base(ref base) => { + Box::new(InheritSceneAsset::from(base.clone())) as Box + } + + BsnPatch::Var(BsnVar(ref symbol, is_template)) => { + let symbol = symbol.clone(); + + let type_registry = app_type_registry.read(); + let resolved_symbol = + symbol.resolve_type_or_enum_variant_to_template(&type_registry, is_template)?; + + let app_type_registry = app_type_registry.clone(); + + Box::new(ErasedTemplatePatch { + template_type_id: resolved_symbol.template_type_id, + app_type_registry: app_type_registry.clone(), + fun: move |reflect, _context| { + // This could be an enum variant + // (`some_crate::Enum::Variant`) or a unit struct. + if !resolved_symbol.template_is_enum { + // This is a unit struct. It should already be instantiated. + return; + } + + let ReflectMut::Enum(enum_reflect) = reflect.reflect_mut() else { + error!("Expected an enum"); + return; + }; + + let dynamic_enum = DynamicEnum::new(symbol.1.clone(), DynamicVariant::Unit); + enum_reflect.apply(&dynamic_enum); + }, + }) as Box + } + + BsnPatch::Struct(BsnStruct(ref symbol, ref fields, is_template)) => { + let symbol = symbol.clone(); + + let type_registry = app_type_registry.read(); + let resolved_symbol = + symbol.resolve_type_or_enum_variant_to_template(&type_registry, is_template)?; + + let template_type_registration = + type_registry.get(resolved_symbol.template_type_id).unwrap(); + let field_infos = if let Ok(structure) = + template_type_registration.type_info().as_struct() + { + StructOrStructVariant::Struct(structure) + } else if let Ok(enumeration) = template_type_registration.type_info().as_enum() { + StructOrStructVariant::StructVariant( + enumeration + .variant(&symbol.1) + .unwrap() + .as_struct_variant()?, + ) + } else { + return Err(DynamicBsnLoaderError::TypeNotStruct); + }; + + let mut dynamic_struct = DynamicStruct::default(); + for field in fields.iter() { + let Some(field_info) = field_infos.get(&field.0) else { + return Err(DynamicBsnLoaderError::StructDoesntHaveField( + field.0.clone(), + )); + }; + let reflect = self.convert_bsn_expr_to_reflect( + field.1, + app_type_registry, + field_info.ty().id(), + )?; + dynamic_struct.insert_boxed(field.0.clone(), reflect); + } + + let app_type_registry = app_type_registry.clone(); + + Box::new(ErasedTemplatePatch { + template_type_id: resolved_symbol.template_type_id, + app_type_registry: app_type_registry.clone(), + fun: move |reflect, _context| { + // This could be an enum variant + // (`some_crate::Enum::Variant`) or a unit struct. + // First, look for a struct. + let struct_type_path = symbol.as_path(); + if !resolved_symbol.template_is_enum { + // This is a struct. + let ReflectMut::Struct(reflect_struct) = reflect.reflect_mut() else { + error!("Expected a struct: `{}`", struct_type_path); + return; + }; + + reflect_struct.apply(&dynamic_struct); + return; + } + + // TODO: struct-like enum variants. Might need to + // convert the `DynamicStruct` into a `DynamicEnum` + // which should be doable + error!("Unknown type: `{}`", struct_type_path); + }, + }) as Box + } + + BsnPatch::NamedTuple(BsnNamedTuple(ref symbol, ref fields, is_template)) => { + let symbol = symbol.clone(); + + let type_registry = app_type_registry.read(); + let resolved_symbol = + symbol.resolve_type_or_enum_variant_to_template(&type_registry, is_template)?; + + let template_type_registration = + type_registry.get(resolved_symbol.template_type_id).unwrap(); + let field_infos = if let Ok(tuple_struct) = + template_type_registration.type_info().as_tuple_struct() + { + tuple_struct.iter() + } else if let Ok(enumeration) = template_type_registration.type_info().as_enum() { + enumeration + .variant(&symbol.1) + .unwrap() + .as_tuple_variant()? + .iter() + } else { + return Err(DynamicBsnLoaderError::TypeNotNamedTuple); + }; + + let mut dynamic_tuple_struct = DynamicTupleStruct::default(); + for (field, field_info) in fields.iter().zip(field_infos) { + let reflect = self.convert_bsn_expr_to_reflect( + *field, + app_type_registry, + field_info.ty().id(), + )?; + dynamic_tuple_struct.insert_boxed(reflect); + } + + let app_type_registry = app_type_registry.clone(); + + Box::new(ErasedTemplatePatch { + template_type_id: resolved_symbol.template_type_id, + app_type_registry: app_type_registry.clone(), + fun: move |reflect, _context| { + // This could be an enum variant + // (`some_crate::Enum::Variant`) or a tuple struct. + // First, look for a struct. + let struct_type_path = symbol.as_path(); + if !resolved_symbol.template_is_enum { + // This is a struct. + let ReflectMut::TupleStruct(reflect_tuple_struct) = + reflect.reflect_mut() + else { + error!("Expected a tuple struct: `{}`", struct_type_path); + return; + }; + + reflect_tuple_struct.apply(&dynamic_tuple_struct); + return; + } + + // TODO: struct-like enum variants. Might need to + // convert the `DynamicStruct` into a `DynamicEnum` + // which should be doable + error!("Unknown type: `{}`", struct_type_path); + }, + }) as Box + } + + BsnPatch::Relation(BsnRelation(ref relation_symbol, ref patches)) => { + // FIXME: What a hack! + if &*relation_symbol.as_path() != "bevy_ecs::hierarchy::Children" { + return Err(DynamicBsnLoaderError::OnlyChildRelationsSupported); + } + let related_template_list: Vec<_> = patches + .iter() + .map(|patches_id| { + // FIXME: seems fishy to throw away dependencies like this + Ok(self + .convert_bsn_patches_to_patch(*patches_id, app_type_registry)? + .scene) + }) + .collect::, DynamicBsnLoaderError>>()?; + Box::new(DynamicRelatedScenes { + relationship: TypeId::of::(), + related_template_list, + }) as Box + } + }; + + Ok(ScenePatch { + scene: patch, + dependencies: vec![], + resolved: None, + }) + } + + fn convert_bsn_expr_to_reflect( + &self, + expr_id: Entity, + app_type_registry: &AppTypeRegistry, + expected_template_type: TypeId, + ) -> Result, DynamicBsnLoaderError> { + let Some(expr) = self.0.get::(expr_id) else { + return Err(DynamicBsnLoaderError::NoSuchAstNode); + }; + + let type_registry = app_type_registry.read(); + + match *expr { + BsnExpr::Var(BsnVar(ref symbol, is_template)) => { + let resolved_symbol = + symbol.resolve_type_or_enum_variant_to_template(&type_registry, is_template)?; + + let template_type_registration = + type_registry.get(resolved_symbol.template_type_id).unwrap(); + + let mut reflect = + create_reflect_default_from_type_registration(template_type_registration)?; + + // This could be an enum variant + // (`some_crate::Enum::Variant`) or a unit struct. + if !resolved_symbol.template_is_enum { + // This is a unit struct. Just instantiate it. + return Ok(reflect.into_partial_reflect()); + } + + // This is a unit enum variant. + let ReflectMut::Enum(enum_reflect) = reflect.reflect_mut() else { + return Err(DynamicBsnLoaderError::UnknownType( + template_type_registration + .type_info() + .type_path() + .to_owned(), + )); + }; + + let dynamic_enum = DynamicEnum::new(symbol.1.clone(), DynamicVariant::Unit); + enum_reflect.apply(&dynamic_enum); + Ok(reflect.into_partial_reflect()) + } + + BsnExpr::Struct(ref bsn_struct) => { + let resolved_symbol = bsn_struct + .0 + .resolve_type_or_enum_variant_to_template(&type_registry, bsn_struct.2)?; + + let template_type_registration = + type_registry.get(resolved_symbol.template_type_id).unwrap(); + let mut reflect = + create_reflect_default_from_type_registration(template_type_registration)?; + + // This could be an enum variant (`some_crate::Enum::Variant`) + // or a struct. + if !resolved_symbol.template_is_enum { + // This is a struct. + let ReflectMut::Struct(reflect_struct) = reflect.reflect_mut() else { + return Err(DynamicBsnLoaderError::UnknownType( + template_type_registration + .type_info() + .type_path() + .to_owned(), + )); + }; + + let Ok(struct_info) = template_type_registration.type_info().as_struct() else { + return Err(DynamicBsnLoaderError::TypeNotStruct); + }; + + let mut dynamic_struct = DynamicStruct::default(); + for field in &bsn_struct.1 { + let Some(field_info) = struct_info.field(&field.0) else { + return Err(DynamicBsnLoaderError::StructDoesntHaveField( + field.0.clone(), + )); + }; + let reflect = self.convert_bsn_expr_to_reflect( + field.1, + app_type_registry, + field_info.ty().id(), + )?; + dynamic_struct.insert_boxed(field.0.clone(), reflect); + } + reflect_struct.apply(&dynamic_struct); + return Ok(reflect.into_partial_reflect()); + } + + // TODO: struct-like enum variants. Might need to + // convert the `DynamicStruct` into a `DynamicEnum` + // which should be doable + Err(DynamicBsnLoaderError::UnknownType( + template_type_registration + .type_info() + .type_path() + .to_owned(), + )) + } + + BsnExpr::NamedTuple(ref named_tuple) => { + let resolved_symbol = named_tuple + .0 + .resolve_type_or_enum_variant_to_template(&type_registry, named_tuple.2)?; + + let template_type_registration = + type_registry.get(resolved_symbol.template_type_id).unwrap(); + let mut reflect = + create_reflect_default_from_type_registration(template_type_registration)?; + + let Ok(tuple_info) = template_type_registration.type_info().as_tuple_struct() + else { + return Err(DynamicBsnLoaderError::TypeNotNamedTuple); + }; + + let mut dynamic_tuple_struct = DynamicTupleStruct::default(); + for (field_id, field_info) in named_tuple.1.iter().zip(tuple_info.iter()) { + let reflect_val = self.convert_bsn_expr_to_reflect( + *field_id, + app_type_registry, + field_info.ty().id(), + )?; + dynamic_tuple_struct.insert_boxed(reflect_val); + } + + if let ReflectMut::TupleStruct(ts) = reflect.reflect_mut() { + ts.apply(&dynamic_tuple_struct); + } + Ok(reflect.into_partial_reflect()) + } + + BsnExpr::StringLit(ref string) => { + let expected_type_registration = type_registry.get(expected_template_type).unwrap(); + let mut reflect = + create_reflect_default_from_type_registration(expected_type_registration)?; + + // TODO: Support `&str`, `Cow`, `Arc`, etc. too? + if expected_template_type == TypeId::of::() { + reflect.apply(string); + return Ok(reflect.into_partial_reflect()); + } + + // FIXME: This is a total hack. We should have a generic + // `ReflectConvert` or `ReflectFrom` or something. + if expected_type_registration + .type_info() + .type_path() + .starts_with("bevy_asset::handle::HandleTemplate<") + { + let asset_path: AssetPath<'static> = AssetPath::parse(string).into_owned(); + let ReflectMut::Enum(reflect_enum) = reflect.reflect_mut() else { + panic!("`HandleTemplate` wasn't an enum") + }; + // `HandleTemplate::Path` is the default, so we don't have + // to set it. + reflect_enum.field_at_mut(0).unwrap().apply(&asset_path); + return Ok(reflect.into_partial_reflect()); + } + + Err(DynamicBsnLoaderError::TypeMismatch) + } + + BsnExpr::FloatLit(float_lit) => { + let mut reflect = create_reflect_default(&type_registry, expected_template_type)?; + + if expected_template_type == TypeId::of::() { + reflect.apply(&(float_lit as f32)); + return Ok(reflect.into_partial_reflect()); + } + if expected_template_type == TypeId::of::() { + reflect.apply(&float_lit); + return Ok(reflect.into_partial_reflect()); + } + Err(DynamicBsnLoaderError::FloatLitTypeMismatch) + } + + BsnExpr::BoolLit(bool_lit) => { + let mut reflect = create_reflect_default(&type_registry, expected_template_type)?; + + if expected_template_type == TypeId::of::() { + reflect.apply(&bool_lit); + return Ok(reflect.into_partial_reflect()); + } + Err(DynamicBsnLoaderError::TypeMismatch) + } + + BsnExpr::List(ref items) => { + let type_registration = + type_registry.get(expected_template_type).ok_or_else(|| { + DynamicBsnLoaderError::UnknownType(format!( + "TypeId {:?}", + expected_template_type + )) + })?; + let list_info = type_registration + .type_info() + .as_list() + .map_err(|_| DynamicBsnLoaderError::TypeMismatch)?; + let item_type_id = list_info.item_ty().id(); + + let mut dynamic_list = DynamicList::default(); + for &item_id in items { + let reflect = + self.convert_bsn_expr_to_reflect(item_id, app_type_registry, item_type_id)?; + dynamic_list.push_box(reflect); + } + dynamic_list.set_represented_type(Some(type_registration.type_info())); + Ok(Box::new(dynamic_list) as Box) + } + + BsnExpr::IntLit(int_lit) => { + let mut reflect = create_reflect_default(&type_registry, expected_template_type)?; + + if expected_template_type == TypeId::of::() { + reflect.apply(&(int_lit as i8)); + return Ok(reflect.into_partial_reflect()); + } + if expected_template_type == TypeId::of::() { + reflect.apply(&(int_lit as u8)); + return Ok(reflect.into_partial_reflect()); + } + if expected_template_type == TypeId::of::() { + reflect.apply(&(int_lit as i16)); + return Ok(reflect.into_partial_reflect()); + } + if expected_template_type == TypeId::of::() { + reflect.apply(&(int_lit as u16)); + return Ok(reflect.into_partial_reflect()); + } + if expected_template_type == TypeId::of::() { + reflect.apply(&(int_lit as i32)); + return Ok(reflect.into_partial_reflect()); + } + if expected_template_type == TypeId::of::() { + reflect.apply(&(int_lit as u32)); + return Ok(reflect.into_partial_reflect()); + } + if expected_template_type == TypeId::of::() { + reflect.apply(&(int_lit as i64)); + return Ok(reflect.into_partial_reflect()); + } + if expected_template_type == TypeId::of::() { + reflect.apply(&(int_lit as u64)); + return Ok(reflect.into_partial_reflect()); + } + if expected_template_type == TypeId::of::() { + reflect.apply(&(int_lit as isize)); + return Ok(reflect.into_partial_reflect()); + } + if expected_template_type == TypeId::of::() { + reflect.apply(&(int_lit as usize)); + return Ok(reflect.into_partial_reflect()); + } + Err(DynamicBsnLoaderError::IntLitTypeMismatch) + } + } + } + + pub fn create_patches(&mut self, patches: Vec) -> Entity { + self.0.spawn(BsnPatches(patches)).id() + } + + pub fn create_patch(&mut self, patch: BsnPatch) -> Entity { + self.0.spawn(patch).id() + } + + pub fn create_expr(&mut self, expr: BsnExpr) -> Entity { + self.0.spawn(expr).id() + } + + pub fn create_name_patch(&mut self, name: String) -> Entity { + let mut name_store = self.0.resource_mut::(); + let index = match name_store.name_indices.get(&*name) { + Some(index) => *index, + None => { + let index = name_store.next_name_index; + name_store.next_name_index += 1; + name_store.name_indices.insert(name.clone(), index); + index + } + }; + self.create_patch(BsnPatch::Name(name, index)) + } +} + +fn create_reflect_default( + type_registry: &TypeRegistry, + expected_template_type: TypeId, +) -> Result, DynamicBsnLoaderError> { + let expected_type_registration = type_registry.get(expected_template_type).unwrap(); + create_reflect_default_from_type_registration(expected_type_registration) +} + +fn create_reflect_default_from_type_registration( + expected_type_registration: &TypeRegistration, +) -> Result, DynamicBsnLoaderError> { + let Some(reflect_default) = expected_type_registration.data::() else { + return Err(DynamicBsnLoaderError::TypeDoesntImplementDefault( + expected_type_registration + .type_info() + .type_path() + .to_owned(), + )); + }; + Ok(reflect_default.default()) +} + +pub struct MultiPatch(Vec>); + +impl Scene for MultiPatch { + fn resolve( + &self, + context: &mut ResolveContext, + scene: &mut ResolvedScene, + ) -> Result<(), ResolveSceneError> { + for subscene in self.0.iter() { + subscene.resolve(context, scene)?; + } + + Ok(()) + } + + fn register_dependencies(&self, dependencies: &mut SceneDependencies) { + for subscene in self.0.iter() { + subscene.register_dependencies(dependencies); + } + } +} + +pub struct DynamicRelatedScenes { + relationship: TypeId, + related_template_list: Vec>, +} + +impl Scene for DynamicRelatedScenes { + fn resolve( + &self, + context: &mut ResolveContext, + scene: &mut ResolvedScene, + ) -> Result<(), ResolveSceneError> { + if self.relationship != TypeId::of::() { + return Err(ResolveSceneError::UnsupportedRelationship); + } + + let related = scene.related.entry(self.relationship).or_insert_with(|| { + RelatedResolvedScenes { + scenes: vec![], + insert: |entity, target| { + // TODO: There should probably be a `ReflectRelationship` + let child_of = ChildOf(target); + entity.insert(child_of); + }, + relationship_name: "ChildOf", + } + }); + + for scene in self.related_template_list.iter() { + let mut resolved_scene = ResolvedScene::default(); + scene.resolve(context, &mut resolved_scene)?; + related.scenes.push(resolved_scene); + } + + Ok(()) + } + + fn register_dependencies(&self, dependencies: &mut SceneDependencies) { + for scene in self.related_template_list.iter() { + scene.register_dependencies(dependencies); + } + } +} + +impl BsnSymbol { + fn resolve_type_or_enum_variant_to_template( + &self, + type_registry: &TypeRegistry, + is_template: bool, + ) -> Result { + // First, look for a unit struct. + let unit_struct_type_path = self.as_path(); + if let Some(type_registration) = type_registry.get_with_type_path(&unit_struct_type_path) { + return Ok(ResolvedSymbol::new(type_registration, false, is_template)); + } + + // Next, look for a unit enum variant. + let Some(enum_type_path) = self.as_path_skip_last() else { + return Err(DynamicBsnLoaderError::UnknownType( + unit_struct_type_path.to_owned(), + )); + }; + let Some(type_registration) = type_registry.get_with_type_path(&enum_type_path) else { + return Err(DynamicBsnLoaderError::UnknownType( + enum_type_path.to_owned(), + )); + }; + Ok(ResolvedSymbol::new(type_registration, true, is_template)) + } + + pub(crate) fn as_path(&self) -> String { + let mut path = String::new(); + for component in &self.0 { + let _ = write!(&mut path, "{}::", &**component); + } + path.push_str(&self.1); + path + } + + fn as_path_skip_last(&self) -> Option { + if self.0.is_empty() { + return None; + } + let mut enum_type_path = String::new(); + for component_index in 0..(self.0.len() - 1) { + let _ = write!(&mut enum_type_path, "{}::", self.0[component_index]); + } + enum_type_path.push_str(self.0.last().unwrap()); + Some(enum_type_path) + } +} + +struct ErasedTemplatePatch +where + F: Fn(&mut dyn PartialReflect, &mut ResolveContext), +{ + pub fun: F, + pub template_type_id: TypeId, + // FIXME: Not a good place for this. Put it in the patch context instead? + pub app_type_registry: AppTypeRegistry, +} + +struct DefaultDynamicErasedTemplate(Box); + +impl Scene for ErasedTemplatePatch +where + F: Fn(&mut dyn PartialReflect, &mut ResolveContext) + Send + Sync + 'static, +{ + fn resolve( + &self, + context: &mut ResolveContext, + scene: &mut ResolvedScene, + ) -> Result<(), ResolveSceneError> { + let template_type_id = self.template_type_id; + let app_type_registry = self.app_type_registry.clone(); + + // Verify that everything is OK before we enter the closure below and + // start unwrapping things. + { + let type_registry = app_type_registry.read(); + let Some(template_type_registration) = type_registry.get(template_type_id) else { + return Err(ResolveSceneError::TypeNotReflectable); + }; + if !template_type_registration.contains::() { + return Err(ResolveSceneError::TypeDoesntReflectDefault); + }; + } + + let template = + scene.get_or_insert_erased_template(context, self.template_type_id, move || { + let reflect = { + let type_registry = app_type_registry.read(); + let type_registration = type_registry.get(template_type_id).unwrap(); + let reflect_default = type_registration.data::().unwrap(); + reflect_default.default() + }; + Box::new(DefaultDynamicErasedTemplate(reflect)) + }); + let Some(reflect) = template.try_as_partial_reflect_mut() else { + return Err(ResolveSceneError::TypeNotReflectable); + }; + (self.fun)(reflect, context); + + Ok(()) + } +} + +impl ErasedTemplate for DefaultDynamicErasedTemplate { + fn apply(&self, context: &mut TemplateContext) -> EcsResult<(), BevyError> { + let maybe_build_template = { + let app_type_registry = context.resource::(); + let type_registry = app_type_registry.read(); + let Some(template_type_registration) = type_registry.get(self.0.as_any().type_id()) + else { + return Err("Template type wasn't registered".into()); + }; + template_type_registration + .data::() + .map(|reflect_template| reflect_template.build_template) + }; + + // If the template type supports `ReflectTemplate`, then call its build + // function. Otherwise, just clone it, under the assumption that the + // template type is the output type. + // + // FIXME: This is undoubtedly convenient, but it might not be the right + // thing to do. It feels a bit dodgy. + let output = match maybe_build_template { + Some(build_template) => build_template(&*self.0, context)?, + None => self.0.reflect_clone()?, + }; + + context.entity.insert_reflect(output.into_partial_reflect()); + Ok(()) + } + + fn clone_template(&self) -> Box { + Box::new(DefaultDynamicErasedTemplate( + self.0.reflect_clone().unwrap(), + )) + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self.0.as_any_mut() + } + + fn try_as_partial_reflect_mut(&mut self) -> Option<&mut dyn PartialReflect> { + Some(self.0.as_partial_reflect_mut()) + } +} + +#[derive(Clone)] +struct ResolvedSymbol { + template_type_id: TypeId, + template_is_enum: bool, +} + +impl ResolvedSymbol { + fn new( + type_registration: &TypeRegistration, + template_is_enum: bool, + is_template: bool, + ) -> ResolvedSymbol { + if is_template { + return ResolvedSymbol { + template_type_id: type_registration.type_id(), + template_is_enum, + }; + } + + // Fetch the template type, if available. Otherwise, we assume the + // `FromTemplate` type is the same as the `Template` type (which it + // will be for clonable, `Default`able things). + ResolvedSymbol { + template_type_id: match type_registration.data::() { + Some(reflect_get_template) => reflect_get_template.template_type_id, + None => type_registration.type_id(), + }, + template_is_enum, + } + } +} + +enum StructOrStructVariant<'a> { + Struct(&'a StructInfo), + StructVariant(&'a StructVariantInfo), +} + +impl<'a> StructOrStructVariant<'a> { + fn get(&self, field_name: &str) -> Option<&'a NamedField> { + match *self { + StructOrStructVariant::Struct(structure) => structure.field(field_name), + StructOrStructVariant::StructVariant(struct_variant) => { + struct_variant.field(field_name) + } + } + } +} diff --git a/crates/bevy_scene2/src/dynamic_bsn_grammar.lalrpop b/crates/bevy_scene2/src/dynamic_bsn_grammar.lalrpop new file mode 100644 index 0000000000000..80f07348473b4 --- /dev/null +++ b/crates/bevy_scene2/src/dynamic_bsn_grammar.lalrpop @@ -0,0 +1,125 @@ +use crate::dynamic_bsn::{ + BsnAst, BsnExpr, BsnField, BsnNamedTuple, BsnPatch, BsnRelation, BsnStruct, BsnSymbol, BsnVar, +}; +use crate::dynamic_bsn_lexer; +use bevy_ecs::entity::Entity; +use std::cell::RefCell; + +grammar(ast: &RefCell); + +pub TopLevelPatches: Entity = { + => ast.borrow_mut().create_patches(p), +} + +pub Patches: Entity = { + => ast.borrow_mut().create_patches(p), +}; + +pub Patch: Entity = { + => ast.borrow_mut().create_name_patch(n), + => ast.borrow_mut().create_patch(BsnPatch::Base(b)), + => ast.borrow_mut().create_patch(BsnPatch::Relation(r)), + => ast.borrow_mut().create_patch(BsnPatch::Var(BsnVar(s, a.is_some()))), + "{" "}" => { + ast.borrow_mut().create_patch(BsnPatch::Struct(BsnStruct(s, f, a.is_some()))) + }, + "(" ")" => { + ast.borrow_mut().create_patch(BsnPatch::NamedTuple(BsnNamedTuple(s, n, a.is_some()))) + }, +}; + +Name: String = { + "#" => i, + "#" => s, +}; + +Base: String = { + ":" => s, +}; + +Symbol: BsnSymbol = { + "::" => s.append(n), + => BsnSymbol::from_ident(n), +}; + +Fields: Vec = { + => Vec::new(), + ","? => f, +} + +FieldsInner: Vec = { + => { let mut v = Vec::new(); v.push(t); v }, + "," => { let mut h = h; h.push(t); h }, +}; + +Field: BsnField = { + ":" => BsnField(n, e), +}; + +NamedTupleArgs: Vec = { + => Vec::new(), + ","? => n, +}; + +NamedTupleArgsInner: Vec = { + => { let mut v = Vec::new(); v.push(t); v }, + "," => { let mut h = h; h.push(t); h }, +}; + +Expr: Entity = { + => ast.borrow_mut().create_expr(BsnExpr::Var(BsnVar(s, a.is_some()))), + "{" "}" => { + ast.borrow_mut().create_expr(BsnExpr::Struct(BsnStruct(s, f, a.is_some()))) + }, + "(" ")" => { + ast.borrow_mut().create_expr(BsnExpr::NamedTuple(BsnNamedTuple(s, n, a.is_some()))) + }, + => ast.borrow_mut().create_expr(BsnExpr::StringLit(s)), + => ast.borrow_mut().create_expr(BsnExpr::FloatLit(f)), + => ast.borrow_mut().create_expr(BsnExpr::BoolLit(b)), + => ast.borrow_mut().create_expr(BsnExpr::IntLit(i)), + "[" ","? "]" => { + ast.borrow_mut().create_expr(BsnExpr::List(n)) + }, + "[" "]" => { + ast.borrow_mut().create_expr(BsnExpr::List(Vec::new())) + }, +}; + +Relation: BsnRelation = { + "[" "]" => BsnRelation(s, r), +}; + +Relations: Vec = { + => Vec::new(), + ","? => r, +}; + +RelationsInner: Vec = { + "," => { let mut h = h; h.push(t); h }, + => { let mut v = Vec::new(); v.push(t); v }, +}; + +extern { + type Location = usize; + type Error = dynamic_bsn_lexer::Error; + + enum dynamic_bsn_lexer::Token { + "ident" => dynamic_bsn_lexer::Token::Ident(), + "string_lit" => dynamic_bsn_lexer::Token::StringLit(), + "float_lit" => dynamic_bsn_lexer::Token::FloatLit(), + "bool_lit" => dynamic_bsn_lexer::Token::BoolLit(), + "int_lit" => dynamic_bsn_lexer::Token::IntLit(), + "[" => dynamic_bsn_lexer::Token::LBracket, + "]" => dynamic_bsn_lexer::Token::RBracket, + "(" => dynamic_bsn_lexer::Token::LParen, + ")" => dynamic_bsn_lexer::Token::RParen, + "{" => dynamic_bsn_lexer::Token::LBrace, + "}" => dynamic_bsn_lexer::Token::RBrace, + "," => dynamic_bsn_lexer::Token::Comma, + ":" => dynamic_bsn_lexer::Token::Colon, + "#" => dynamic_bsn_lexer::Token::Hash, + "@" => dynamic_bsn_lexer::Token::At, + "::" => dynamic_bsn_lexer::Token::DoubleColon, + } +} diff --git a/crates/bevy_scene2/src/dynamic_bsn_lexer.rs b/crates/bevy_scene2/src/dynamic_bsn_lexer.rs new file mode 100644 index 0000000000000..8d51da51f45e8 --- /dev/null +++ b/crates/bevy_scene2/src/dynamic_bsn_lexer.rs @@ -0,0 +1,314 @@ +use nom::{IResult, Parser as _}; + +#[derive(Clone, PartialEq, Debug)] +pub enum Token { + Ident(String), + StringLit(String), + IntLit(i128), + FloatLit(f64), + BoolLit(bool), + LBracket, + RBracket, + LParen, + RParen, + LBrace, + RBrace, + Comma, + DoubleColon, + Colon, + Hash, + At, +} + +#[derive(Debug)] +pub enum Error { + UnexpectedChar(char), +} + +pub struct Lexer<'a> { + input: &'a str, + pos: usize, +} + +impl<'a> Lexer<'a> { + pub fn new(input: &'a str) -> Lexer<'a> { + Lexer { input, pos: 0 } + } +} + +fn lex_c_comment(input: &str) -> IResult<&str, ()> { + nom::combinator::value( + (), + nom::sequence::delimited( + nom::bytes::complete::tag("/*"), + nom::multi::many0(nom::branch::alt(( + nom::combinator::map(lex_c_comment, |_| ' '), + nom::character::complete::none_of("*"), + nom::combinator::map( + nom::sequence::terminated( + nom::bytes::complete::tag("*"), + nom::combinator::not(nom::bytes::complete::tag("/")), + ), + |_| '*', + ), + ))), + nom::bytes::complete::tag("*/"), + ), + ) + .parse(input) +} + +fn lex_cpp_comment(input: &str) -> IResult<&str, ()> { + nom::combinator::value( + (), + ( + nom::bytes::complete::tag("//"), + nom::multi::many0(nom::character::complete::none_of("\n")), + nom::character::complete::newline, + ), + ) + .parse(input) +} + +fn lex_ignorable(input: &str) -> IResult<&str, ()> { + nom::combinator::value( + (), + nom::multi::many0(nom::branch::alt(( + nom::bytes::complete::take_while1(|c: char| c.is_ascii_whitespace()), + nom::combinator::map(lex_c_comment, |_| ""), + nom::combinator::map(lex_cpp_comment, |_| ""), + ))), + ) + .parse(input) +} + +fn lex_l_paren(input: &str) -> IResult<&str, Token> { + nom::combinator::value(Token::LParen, nom::character::complete::char('(')).parse(input) +} +fn lex_r_paren(input: &str) -> IResult<&str, Token> { + nom::combinator::value(Token::RParen, nom::character::complete::char(')')).parse(input) +} +fn lex_l_bracket(input: &str) -> IResult<&str, Token> { + nom::combinator::value(Token::LBracket, nom::character::complete::char('[')).parse(input) +} +fn lex_r_bracket(input: &str) -> IResult<&str, Token> { + nom::combinator::value(Token::RBracket, nom::character::complete::char(']')).parse(input) +} +fn lex_l_brace(input: &str) -> IResult<&str, Token> { + nom::combinator::value(Token::LBrace, nom::character::complete::char('{')).parse(input) +} +fn lex_r_brace(input: &str) -> IResult<&str, Token> { + nom::combinator::value(Token::RBrace, nom::character::complete::char('}')).parse(input) +} +fn lex_hash(input: &str) -> IResult<&str, Token> { + nom::combinator::value(Token::Hash, nom::character::complete::char('#')).parse(input) +} +fn lex_double_colon(input: &str) -> IResult<&str, Token> { + nom::combinator::value(Token::DoubleColon, nom::bytes::complete::tag("::")).parse(input) +} +fn lex_colon(input: &str) -> IResult<&str, Token> { + nom::combinator::value(Token::Colon, nom::character::complete::char(':')).parse(input) +} +fn lex_comma(input: &str) -> IResult<&str, Token> { + nom::combinator::value(Token::Comma, nom::character::complete::char(',')).parse(input) +} +fn lex_at(input: &str) -> IResult<&str, Token> { + nom::combinator::value(Token::At, nom::character::complete::char('@')).parse(input) +} + +fn lex_false(input: &str) -> IResult<&str, Token> { + nom::combinator::value(Token::BoolLit(false), nom::bytes::complete::tag("false")).parse(input) +} +fn lex_true(input: &str) -> IResult<&str, Token> { + nom::combinator::value(Token::BoolLit(true), nom::bytes::complete::tag("true")).parse(input) +} + +fn lex_ident(ident: &str) -> IResult<&str, Token> { + let (rest, s) = nom::combinator::recognize(nom::sequence::pair( + nom::bytes::complete::take_while1(|c: char| c.is_ascii_alphabetic() || c == '_'), + nom::bytes::complete::take_while(|c: char| c.is_ascii_alphanumeric() || c == '_'), + )) + .parse(ident)?; + Ok((rest, Token::Ident(s.to_owned()))) +} + +fn lex_string(input: &str) -> IResult<&str, Token> { + let (rest, s) = nom::sequence::delimited( + nom::character::complete::char('"'), + string_body, + nom::character::complete::char('"'), + ) + .parse(input)?; + return Ok((rest, Token::StringLit(s))); + + fn string_body(mut input: &str) -> IResult<&str, String> { + let mut out = String::new(); + + loop { + let (rest, chunk) = + nom::bytes::complete::take_while(|c: char| c != '\\' && c != '"')(input)?; + out.push_str(chunk); + input = rest; + + if input.starts_with('"') || input.is_empty() { + break; + } + + let (rest, _) = nom::character::complete::char('\\')(input)?; + if rest.is_empty() { + out.push('\\'); + input = rest; + break; + } + let esc_char = rest.chars().next().unwrap(); + let rest = &rest[esc_char.len_utf8()..]; + match esc_char { + 'n' => out.push('\n'), + 'r' => out.push('\r'), + 't' => out.push('\t'), + '0' => out.push('\0'), + '\\' => out.push('\\'), + '"' => out.push('"'), + 'x' => { + if rest.len() >= 2 { + let hex = &rest[..2]; + if let Ok(byte) = u8::from_str_radix(hex, 16) { + out.push(byte as char); + input = &rest[2..]; + continue; + } + } + out.push('\\'); + out.push('x'); + input = rest; + continue; + } + other => { + out.push('\\'); + out.push(other); + } + } + input = rest; + } + + Ok((input, out)) + } +} + +fn lex_int(input: &str) -> IResult<&str, Token> { + let (rest, (sign, radix_prefix, digits)) = nom::sequence::tuple(( + nom::combinator::opt(nom::branch::alt(( + nom::bytes::complete::tag("+"), + nom::bytes::complete::tag("-"), + ))), + nom::combinator::opt(nom::bytes::complete::tag_no_case("0x")), + nom::branch::alt(( + nom::bytes::complete::take_while1(|c: char| c.is_ascii_hexdigit()), + nom::bytes::complete::take_while1(|c: char| c.is_ascii_digit()), + )), + )) + .parse(input)?; + + let is_hex = radix_prefix.is_some(); + let is_negative = sign == Some("-"); + let radix = if is_hex { 16 } else { 10 }; + + // Make sure decimal numbers have no hex digits. + if !is_hex + && digits + .chars() + .any(|c| c.is_ascii_hexdigit() && !c.is_ascii_digit()) + { + return Err(nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::Digit, + ))); + } + + match i128::from_str_radix(digits, radix) { + Ok(integer) if is_negative => Ok((rest, Token::IntLit(-integer))), + Ok(integer) => Ok((rest, Token::IntLit(integer))), + Err(_) => Err(nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::Digit, + ))), + } +} + +fn lex_float(input: &str) -> IResult<&str, Token> { + let (rest, raw) = nom::combinator::recognize(nom::sequence::tuple(( + nom::combinator::opt(nom::character::complete::one_of("+-")), + nom::combinator::opt(nom::character::complete::digit1), + nom::character::complete::char('.'), + nom::combinator::opt(nom::character::complete::digit1), + nom::combinator::opt(nom::sequence::tuple(( + nom::character::complete::one_of("eE"), + nom::combinator::opt(nom::character::complete::one_of("+-")), + nom::character::complete::digit1, + ))), + ))) + .parse(input)?; + + // `.` isn't a valid number. + if raw == "." { + return Err(nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::Float, + ))); + } + + let value: f64 = raw.parse().map_err(|_| { + nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Float)) + })?; + + Ok((rest, Token::FloatLit(value))) +} + +fn lex_token(input: &str) -> IResult<&str, Token> { + nom::branch::alt(( + lex_true, // Must come before `lex_ident`. + lex_false, // Must come before `lex_ident`. + lex_ident, + lex_string, + lex_float, // Must come before `lex_int`. + lex_int, + lex_l_bracket, + lex_r_bracket, + lex_l_paren, + lex_r_paren, + lex_l_brace, + lex_r_brace, + lex_comma, + lex_double_colon, // Must come before `lex_colon`. + lex_colon, + lex_hash, + lex_at, + )) + .parse(input) +} + +impl<'a> Iterator for Lexer<'a> { + type Item = Result<(usize, Token, usize), Error>; + + fn next(&mut self) -> Option { + let (rest, _) = lex_ignorable(self.input).unwrap(); + self.pos += self.input.len() - rest.len(); + self.input = rest; + if self.input.is_empty() { + return None; + } + + match lex_token(self.input) { + Ok((rest, token)) => { + let start_pos = self.pos; + let end_pos = start_pos + self.input.len() - rest.len(); + self.input = rest; + self.pos = end_pos; + Some(Ok((start_pos, token, end_pos))) + } + Err(_) => Some(Err(Error::UnexpectedChar( + self.input.chars().next().unwrap(), + ))), + } + } +} diff --git a/crates/bevy_scene2/src/lib.rs b/crates/bevy_scene2/src/lib.rs index a2a66c7525932..89426c986cb3e 100644 --- a/crates/bevy_scene2/src/lib.rs +++ b/crates/bevy_scene2/src/lib.rs @@ -15,6 +15,9 @@ pub mod macro_utils; extern crate alloc; +pub mod bsn_asset_catalog; +mod dynamic_bsn; +mod dynamic_bsn_lexer; mod resolved_scene; mod scene; mod scene_list; @@ -22,6 +25,8 @@ mod scene_patch; mod spawn; pub use bevy_scene2_macros::*; + +use lalrpop_util::lalrpop_mod; pub use resolved_scene::*; pub use scene::*; pub use scene_list::*; @@ -32,6 +37,13 @@ use bevy_app::{App, Plugin, SceneSpawnerSystems, SpawnScene}; use bevy_asset::AssetApp; use bevy_ecs::prelude::*; +use crate::dynamic_bsn::DynamicBsnLoader; + +lalrpop_mod!( + #[allow(unused_qualifications)] + dynamic_bsn_grammar +); + /// Adds support for spawning Bevy Scenes. See [`Scene`], [`SceneList`], [`ScenePatch`], and the [`bsn!`] macro for more information. #[derive(Default)] pub struct ScenePlugin; @@ -41,6 +53,7 @@ impl Plugin for ScenePlugin { app.init_resource::() .init_asset::() .init_asset::() + .init_asset_loader::() .add_systems( SpawnScene, (resolve_scene_patches, spawn_queued) diff --git a/crates/bevy_scene2/src/resolved_scene.rs b/crates/bevy_scene2/src/resolved_scene.rs index a2b968d69e298..93c02e18862b8 100644 --- a/crates/bevy_scene2/src/resolved_scene.rs +++ b/crates/bevy_scene2/src/resolved_scene.rs @@ -111,7 +111,7 @@ pub struct ResolvedScene { /// /// [`Children`]: bevy_ecs::hierarchy::Children // PERF: special casing Children might make sense here to avoid hashing - related: TypeIdMap, + pub related: TypeIdMap, /// The inherited [`ScenePatch`] to apply _first_ before applying this [`ResolvedScene`]. inherited: Option>, /// A [`TypeId`] to `templates` index mapping. If a [`Template`] is intended to be shared / patched across scenes, it should be registered @@ -216,14 +216,6 @@ impl ResolvedScene { Ok(()) } - /// This will get the [`Template`], if it already exists in this [`ResolvedScene`]. If it doesn't exist, - /// it will use [`Default`] to create a new [`Template`]. - /// - /// This uses "copy-on-write" behavior for inherited scenes. If a [`Template`] that the inherited scene has is requested, it will be - /// cloned (using [`Template::clone_template`]), added to the current [`ResolvedScene`], and returned. - /// - /// This will ignore [`Template`]s added to this scene using [`ResolvedScene::push_template`], as these are not registered as the "canonical" - /// [`Template`] for a given [`TypeId`]. pub fn get_or_insert_template< 'a, T: Template + Default + Send + Sync + 'static, @@ -232,6 +224,7 @@ impl ResolvedScene { context: &mut ResolveContext, ) -> &'a mut T { self.get_or_insert_erased_template(context, TypeId::of::(), || Box::new(T::default())) + .as_any_mut() .downcast_mut() .unwrap() } @@ -245,12 +238,15 @@ impl ResolvedScene { /// /// This will ignore [`Template`]s added to this scene using [`ResolvedScene::push_template`], as these are not registered as the "canonical" /// [`Template`] for a given [`TypeId`]. - pub fn get_or_insert_erased_template<'a>( + pub fn get_or_insert_erased_template<'a, F>( &'a mut self, context: &mut ResolveContext, type_id: TypeId, - default: fn() -> Box, - ) -> &'a mut dyn ErasedTemplate { + default: F, + ) -> &'a mut dyn ErasedTemplate + where + F: Fn() -> Box, + { self.internal_get_or_insert_template_with(type_id, || { if let Some(inherited_scene) = context.inherited && let Some(resolved_inherited) = &inherited_scene.resolved diff --git a/crates/bevy_scene2/src/scene.rs b/crates/bevy_scene2/src/scene.rs index 12dd7adaf4d68..ed52817d089c2 100644 --- a/crates/bevy_scene2/src/scene.rs +++ b/crates/bevy_scene2/src/scene.rs @@ -103,6 +103,12 @@ pub enum ResolveSceneError { /// Caused when a dependency listed in [`Scene::register_dependencies`] is not available when calling [`Scene::resolve`] #[error("Cannot resolve scene because the asset dependency {0} is not present. This could be because it isn't loaded yet, or because the asset does not exist. Consider using `queue_spawn_scene()` if you would like to wait for scene dependencies before spawning.")] MissingSceneDependency(AssetPath<'static>), + #[error("Cannot resolve scene because an unsupported relationship was used")] + UnsupportedRelationship, + #[error("Cannot resolve scene because a type wasn't reflectable")] + TypeNotReflectable, + #[error("Cannot resolve scene because a type didn't reflect `Default`")] + TypeDoesntReflectDefault, /// Caused when inheriting a scene during [`Scene::resolve`] fails. #[error(transparent)] InheritSceneError(#[from] InheritSceneError), @@ -205,6 +211,20 @@ all_tuples!(scene_impl, 0, 12, P); /// ``` pub struct TemplatePatch(pub F, pub PhantomData); +impl Scene for Box { + fn resolve( + &self, + context: &mut ResolveContext, + scene: &mut ResolvedScene, + ) -> Result<(), ResolveSceneError> { + (**self).resolve(context, scene) + } + + fn register_dependencies(&self, dependencies: &mut SceneDependencies) { + (**self).register_dependencies(dependencies); + } +} + /// Returns a [`Scene`] that completely overwrites the current value of a [`Template`] `T` with the given `value`. /// The `value` is cloned each time the [`Template`] is built. pub fn template_value( diff --git a/crates/bevy_scene2/src/scene_list.rs b/crates/bevy_scene2/src/scene_list.rs index d47039278f9b9..b77596ae6bd5a 100644 --- a/crates/bevy_scene2/src/scene_list.rs +++ b/crates/bevy_scene2/src/scene_list.rs @@ -100,6 +100,7 @@ impl SceneList for Vec { } } +/* impl SceneList for Vec> { fn resolve_list( &self, @@ -120,3 +121,4 @@ impl SceneList for Vec> { } } } +*/ diff --git a/examples/3d/light_probe_blending.rs b/examples/3d/light_probe_blending.rs index b2656c1c32c1e..12e783c6dd535 100644 --- a/examples/3d/light_probe_blending.rs +++ b/examples/3d/light_probe_blending.rs @@ -324,7 +324,7 @@ fn spawn_light_probes(commands: &mut Commands, asset_server: &AssetServer) { diffuse_map: asset_server.load(get_web_asset_url("diffuse_room1.ktx2")), specular_map: asset_server.load(get_web_asset_url("specular_room1.ktx2")), intensity: LIGHT_PROBE_INTENSITY, - ..default() + ..EnvironmentMapLight::default() }, Transform::from_scale(vec3(1.0, -1.0, 1.0) * LIGHT_PROBE_SIDE_LENGTH) .with_rotation(Quat::from_rotation_x(PI)), @@ -340,7 +340,7 @@ fn spawn_light_probes(commands: &mut Commands, asset_server: &AssetServer) { diffuse_map: asset_server.load(get_web_asset_url("diffuse_room2.ktx2")), specular_map: asset_server.load(get_web_asset_url("specular_room2.ktx2")), intensity: LIGHT_PROBE_INTENSITY, - ..default() + ..EnvironmentMapLight::default() }, Transform::from_scale(vec3(1.0, -1.0, 1.0) * LIGHT_PROBE_SIDE_LENGTH) .with_rotation(Quat::from_rotation_x(PI)) diff --git a/examples/3d/pbr.rs b/examples/3d/pbr.rs index 5bfcf6fe6e67e..541ebe34aad6b 100644 --- a/examples/3d/pbr.rs +++ b/examples/3d/pbr.rs @@ -119,7 +119,7 @@ fn setup( diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), intensity: 900.0, - ..default() + ..EnvironmentMapLight::default() }, )); } diff --git a/examples/3d/pccm.rs b/examples/3d/pccm.rs index 08f04c077268e..db0467ff83ac7 100644 --- a/examples/3d/pccm.rs +++ b/examples/3d/pccm.rs @@ -141,7 +141,7 @@ fn spawn_reflection_probe(commands: &mut Commands, asset_server: &AssetServer) { diffuse_map, specular_map, intensity: ENVIRONMENT_MAP_INTENSITY, - ..default() + ..EnvironmentMapLight::default() }, // HACK: slightly larger than 10.0 to avoid z-fighting from the outer cube // faces being partially inside and partially outside the light probe influence diff --git a/examples/3d/post_processing.rs b/examples/3d/post_processing.rs index e472bcdb97d2f..e969f2a55fbdb 100644 --- a/examples/3d/post_processing.rs +++ b/examples/3d/post_processing.rs @@ -86,7 +86,7 @@ fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) { diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), intensity: 2000.0, - ..default() + ..EnvironmentMapLight::default() }, // Include the `ChromaticAberration` component. ChromaticAberration::default(), diff --git a/examples/README.md b/examples/README.md index 7fa24bd3acca8..4b74ad627776a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -468,6 +468,7 @@ Example | Description Example | Description --- | --- +[BSN Asset Catalog](../examples/scene/bsn_asset_catalog.rs) | Demonstrates loading named material definitions from a BSN asset catalog [BSN example](../examples/scene/bsn.rs) | Demonstrates how to use BSN to compose scenes [Scene](../examples/scene/scene.rs) | Demonstrates loading from and saving scenes to files diff --git a/examples/gltf/load_gltf.rs b/examples/gltf/load_gltf.rs index 2e6f790a95802..b674f75d1c95c 100644 --- a/examples/gltf/load_gltf.rs +++ b/examples/gltf/load_gltf.rs @@ -16,14 +16,16 @@ fn main() { } fn setup(mut commands: Commands, asset_server: Res) { + let camera_transform = + Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y); commands.spawn(( Camera3d::default(), - Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), + camera_transform, EnvironmentMapLight { diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), intensity: 250.0, - ..default() + ..EnvironmentMapLight::default() }, )); diff --git a/examples/large_scenes/bistro/src/main.rs b/examples/large_scenes/bistro/src/main.rs index 23820a01a8d63..ad32049cb8637 100644 --- a/examples/large_scenes/bistro/src/main.rs +++ b/examples/large_scenes/bistro/src/main.rs @@ -279,7 +279,7 @@ pub fn setup(mut commands: Commands, asset_server: Res, args: Res)>, + logged: bool, +} + +fn setup( + mut commands: Commands, + asset_server: Res, + mut meshes: ResMut>, +) { + let sphere = meshes.add(Sphere::new(0.5).mesh().ico(5).unwrap()); + + let catalog_entries: &[(&str, &str)] = &[ + ("PolishedMetal", "Polished Metal"), + ("BrushedMetal", "Brushed Metal"), + ("RoughStone", "Rough Stone"), + ("Plastic", "Plastic"), + ]; + + let spacing = 1.5; + let offset = (catalog_entries.len() as f32 - 1.0) * spacing / 2.0; + let mut handles = Vec::new(); + + for (i, (catalog_name, display_name)) in catalog_entries.iter().enumerate() { + let x = i as f32 * spacing - offset; + let material: Handle = + asset_server.load(format!("scenes/material_catalog.bsn#{catalog_name}")); + + handles.push((catalog_name.to_string(), material.clone())); + + // Sphere + commands.spawn(( + Mesh3d(sphere.clone()), + MeshMaterial3d(material), + Transform::from_xyz(x, 0.5, 0.0), + )); + + // Label + commands.spawn(( + Text2d::new(*display_name), + TextFont::from_font_size(14.0), + Transform::from_xyz(x, -0.3, 0.0), + )); + } + + commands.insert_resource(CatalogMaterials { + handles, + logged: false, + }); + + // Ground plane + commands.spawn(( + Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(5.0)))), + MeshMaterial3d::::default(), + )); + + // Lighting + commands.spawn(( + DirectionalLight { + illuminance: 5000.0, + shadow_maps_enabled: true, + ..default() + }, + Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 0.7, -FRAC_PI_4)), + )); + + commands.spawn(( + EnvironmentMapLight { + diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), + specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), + intensity: 500.0, + rotation: Quat::IDENTITY, + affects_lightmapped_mesh_diffuse: false, + }, + Transform::default(), + )); + + // Camera + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0.0, 2.0, 5.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), + )); + + info!("Press S to serialize the loaded materials back to BSN text"); +} + +/// Log material properties once they finish loading, as verification. +fn check_materials_loaded( + mut catalog: ResMut, + materials: Res>, +) { + if catalog.logged { + return; + } + + let all_loaded = catalog + .handles + .iter() + .all(|(_, h)| materials.get(h).is_some()); + if !all_loaded { + return; + } + + catalog.logged = true; + info!("All catalog materials loaded:"); + for (name, handle) in &catalog.handles { + if let Some(mat) = materials.get(handle) { + info!( + " #{name}: metallic={:.2}, roughness={:.2}, reflectance={:.2}", + mat.metallic, mat.perceptual_roughness, mat.reflectance + ); + } + } +} + +/// Press S to serialize the loaded materials back to BSN catalog text. +fn save_catalog_on_keypress( + input: Res>, + catalog: Res, + materials: Res>, + world: &World, +) { + if !input.just_pressed(KeyCode::KeyS) { + return; + } + + let asset_refs: Vec<_> = catalog + .handles + .iter() + .filter_map(|(name, handle)| { + materials.get(handle)?; + Some(CatalogAssetRef { + name: name.clone(), + type_id: std::any::TypeId::of::(), + asset_id: handle.id().untyped(), + }) + }) + .collect(); + + let bsn_text = serialize_assets_to_bsn(world, &asset_refs); + info!("Serialized catalog to BSN:\n{bsn_text}"); +} diff --git a/examples/scene/dynamic_bsn.rs b/examples/scene/dynamic_bsn.rs new file mode 100644 index 0000000000000..096693d56682f --- /dev/null +++ b/examples/scene/dynamic_bsn.rs @@ -0,0 +1,57 @@ +//! Demonstrates how to load and spawn BSN assets at runtime. + +use std::f32::consts::{FRAC_PI_4, PI}; + +use bevy::ecs::reflect::{ReflectFromTemplate, ReflectTemplate}; +use bevy::light::CascadeShadowConfig; +use bevy::prelude::*; +use bevy_scene2::ScenePatchInstance; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, animate_light_direction) + .run(); +} + +#[derive(Clone, Copy, Default, Component, Debug, Reflect)] +#[reflect(Clone, Default, Component)] +enum TestEnum { + #[default] + Foo, + Bar, + Baz, +} + +#[derive(Clone, Default, Component, Debug, Reflect)] +#[reflect(Default, Component)] +struct TestStruct { + the_enum: TestEnum, +} + +#[derive(Clone, Component, Debug, Reflect, FromTemplate)] +#[reflect(Clone, Component, FromTemplate)] +#[template(reflect)] +struct HandleContainer { + field: Handle, +} + +fn setup(mut commands: Commands, asset_server: Res) { + let scene_patch = asset_server.load("scenes/example.bsn"); + commands.spawn(ScenePatchInstance(scene_patch)); +} + +fn animate_light_direction( + time: Res