//! Shared editor/game authoring data and hydration. //! //! Saved levels are Bevy dynamic scenes made from these small, reflectable //! authoring components. Runtime-only render/physics components are generated //! from them by hydration systems in both the game and editor. mod actor; mod animation; pub mod brush_math; mod components; mod hydration; mod material_asset; mod navigation; mod post_process_effect_asset; mod prefab_overrides; mod renderer_material; mod rendering_profile_asset; pub use actor::{infer_actor_kind, validate_actor, ActorValidationError}; pub use animation::*; pub use components::*; pub use hydration::{ cascade_config_from_rendering, flush_level_object_hydration, material_from_desc, strip_hydrated, strip_hydrated_entity, HydratedModelRoot, HydratedPrefabMember, HydratedPrefabReady, HydratedSkinnedMeshRoot, HydrationPlugin, PrefabHydrationBlocked, }; pub use material_asset::{ load_resolved_material_from_path, MaterialAlphaMode, MaterialAsset, MaterialInstanceAsset, MaterialRenderState, ShaderPropertyDesc, ShaderPropertyType, ShaderSchemaAsset, MATERIAL_ASSET_SCHEMA_VERSION, MATERIAL_INSTANCE_SCHEMA_VERSION, SURFACE_SHADER_SCHEMA_VERSION, }; pub use navigation::*; pub use post_process_effect_asset::{PostProcessEffectAsset, PostProcessEffectKind}; pub use prefab_overrides::{ apply_prefab_overrides_on_ready, apply_serialized_navigation_property_override, decode_prefab_overrides, encode_prefab_overrides, PrefabActorPath, PrefabComponentOverride, PrefabOverrides, PrefabPropertyOverride, PrefabStructuralOverride, PREFAB_OVERRIDE_FORMAT_VERSION, }; pub use renderer_material::*; pub use rendering_profile_asset::RenderingProfileAsset; /// Converts catalog-style paths (`assets/models/foo.fbx`) to Bevy AssetServer paths. pub fn asset_server_path(catalog_path: &str) -> String { let normalized = catalog_path.replace('\\', "/"); normalized .strip_prefix("assets/") .map(str::to_string) .unwrap_or(normalized) } /// Returns the typed [`StandardMaterial`] asset path for a source material label. /// /// Bevy's glTF loader owns `#MaterialN` as a `GltfMaterial`; the PBR extension /// publishes its renderable conversion under the nested `#MaterialN/std` label. /// Other model loaders, including `bevy_ufbx`, expose `StandardMaterial` at the /// original label. pub fn standard_material_asset_path(asset_path: &str, material_label: &str) -> String { let lower_path = asset_path.to_ascii_lowercase(); let gltf_source = lower_path.ends_with(".gltf") || lower_path.ends_with(".glb"); let label = if gltf_source && !material_label.ends_with("/std") { format!("{material_label}/std") } else { material_label.to_string() }; format!("{asset_path}#{label}") } use bevy::prelude::*; /// Registers all reflectable authoring types and installs hydration systems. pub struct SharedTypesPlugin; impl Plugin for SharedTypesPlugin { fn build(&self, app: &mut App) { app.register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .add_systems(Update, migrate_legacy_authoring_component_states) .add_plugins(HydrationPlugin); } } #[allow(clippy::type_complexity)] fn migrate_legacy_authoring_component_states( mut commands: Commands, mut legacy: Query< (Entity, &mut InspectorOrder), (With, Without), >, ) { for (entity, mut order) in &mut legacy { let mut states = std::mem::take(&mut order.component_states); for state in &mut states { if state.component_id.is_empty() { if let Some(component_id) = authoring_component_id(&state.type_name) { state.component_id = component_id.to_string(); state.type_name.clear(); } } } commands .entity(entity) .insert(AuthoringComponentStates { states }); } } #[cfg(test)] mod tests { use super::*; #[test] fn gltf_standard_material_paths_use_pbr_conversion_label() { assert_eq!( standard_material_asset_path("models/robot.gltf", "Material3"), "models/robot.gltf#Material3/std" ); assert_eq!( standard_material_asset_path("models/robot.GLB", "DefaultMaterial"), "models/robot.GLB#DefaultMaterial/std" ); } #[test] fn non_gltf_material_paths_keep_loader_label() { assert_eq!( standard_material_asset_path("models/robot.fbx", "Material3"), "models/robot.fbx#Material3" ); } #[test] fn converted_gltf_material_label_is_not_suffixed_twice() { assert_eq!( standard_material_asset_path("models/robot.glb", "Material3/std"), "models/robot.glb#Material3/std" ); } #[test] fn legacy_inspector_active_state_moves_to_authoring_component_states() { let mut app = App::new(); app.add_systems(Update, migrate_legacy_authoring_component_states); let mut order = InspectorOrder::default(); order.component_states.push(InspectorComponentState { component_id: String::new(), type_name: COMPONENT_LIGHT_DESC.to_string(), active: false, }); let entity = app.world_mut().spawn((LevelObject, order)).id(); app.update(); let states = app.world().get::(entity).unwrap(); assert!(!states.is_component_active(COMPONENT_LIGHT_DESC)); assert_eq!(states.states[0].component_id, AUTHORING_COMPONENT_LIGHT); assert!(states.states[0].type_name.is_empty()); assert!(app .world() .get::(entity) .unwrap() .component_states .is_empty()); } #[test] fn new_active_state_takes_precedence_over_legacy_order() { let mut legacy = InspectorOrder::default(); legacy.set_component_active(COMPONENT_LIGHT_DESC, false); let mut states = AuthoringComponentStates::default(); states.set_component_active(COMPONENT_LIGHT_DESC, true); assert!(authoring_component_active( Some(&states), Some(&legacy), COMPONENT_LIGHT_DESC )); } #[test] fn inspector_order_migrates_legacy_type_paths_to_stable_ids_on_edit() { let present = [COMPONENT_LIGHT_DESC, COMPONENT_PRIMITIVE]; let mut order = InspectorOrder { component_type_names: present.iter().map(|value| value.to_string()).collect(), ..Default::default() }; assert_eq!(order.ordered_components(&present), present); assert!(order.move_component(COMPONENT_PRIMITIVE, -1, &present)); assert!(order.component_type_names.is_empty()); assert_eq!( order.component_ids, vec![ AUTHORING_COMPONENT_PRIMITIVE.to_string(), AUTHORING_COMPONENT_LIGHT.to_string() ] ); assert_eq!( order.ordered_components(&present), vec![COMPONENT_PRIMITIVE, COMPONENT_LIGHT_DESC] ); } }