274 lines
10 KiB
Rust
274 lines
10 KiB
Rust
//! 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::<ActorKind>()
|
|
.register_type::<ActorId>()
|
|
.register_type::<ActorName>()
|
|
.register_type::<SceneComposition>()
|
|
.register_type::<SubsceneReference>()
|
|
.register_type::<HierarchySiblingIndex>()
|
|
.register_type::<InspectorOrder>()
|
|
.register_type::<AuthoringComponentStates>()
|
|
.register_type::<InspectorComponentState>()
|
|
.register_type::<ComponentInstanceId>()
|
|
.register_type::<EditorAssetRef>()
|
|
.register_type::<MaterialRef>()
|
|
.register_type::<RendererMaterialSlot>()
|
|
.register_type::<RendererMaterialSet>()
|
|
.register_type::<OrphanedMaterialAssignment>()
|
|
.register_type::<AnimationSkeletonSignature>()
|
|
.register_type::<SkinnedMeshRenderer>()
|
|
.register_type::<AnimationPlaybackRange>()
|
|
.register_type::<AnimationEventDesc>()
|
|
.register_type::<AnimationStateDesc>()
|
|
.register_type::<AnimationControllerDesc>()
|
|
.register_type::<NavigationAgentProfile>()
|
|
.register_type::<NavigationPathSample>()
|
|
.register_type::<NavigationBounds>()
|
|
.register_type::<NavigationObstacle>()
|
|
.register_type::<NavigationArea>()
|
|
.register_type::<NavigationLink>()
|
|
.register_type::<AudioSourceDesc>()
|
|
.register_type::<AudioListenerDesc>()
|
|
.register_type::<AudioAttenuationDesc>()
|
|
.register_type::<AudioRolloff>()
|
|
.register_type::<EditorVisibility>()
|
|
.register_type::<LevelObject>()
|
|
.register_type::<RaytracingExcluded>()
|
|
.register_type::<PlayerSpawn>()
|
|
.register_type::<ProjectSun>()
|
|
.register_type::<Primitive>()
|
|
.register_type::<PrimitiveShape>()
|
|
.register_type::<BrushDesc>()
|
|
.register_type::<BrushKind>()
|
|
.register_type::<BrushPlaneDesc>()
|
|
.register_type::<BrushFaceDesc>()
|
|
.register_type::<StaticMeshRenderer>()
|
|
.register_type::<StaticMeshRendererEntry>()
|
|
.register_type::<StaticMeshAssetRef>()
|
|
.register_type::<StaticMeshMaterialSlot>()
|
|
.register_type::<StaticMeshColliderMode>()
|
|
.register_type::<MaterialDesc>()
|
|
.register_type::<MaterialShaderKind>()
|
|
.register_type::<ShaderRefDesc>()
|
|
.register_type::<MaterialParameterValue>()
|
|
.register_type::<MaterialParameter>()
|
|
.register_type::<MaterialTextureBinding>()
|
|
.register_type::<MaterialSlotOverride>()
|
|
.register_type::<MaterialOverride>()
|
|
.register_type::<ColorDesc>()
|
|
.register_type::<LightDesc>()
|
|
.register_type::<AuthoringLightKind>()
|
|
.register_type::<RigidBodyDesc>()
|
|
.register_type::<ColliderDesc>()
|
|
.register_type::<ColliderShapeDesc>()
|
|
.register_type::<MeshColliderCooking>()
|
|
.register_type::<PhysicsBody>()
|
|
.register_type::<AuthoringRigidBody>()
|
|
.register_type::<ModelRef>()
|
|
.register_type::<PrefabRef>()
|
|
.register_type::<PrefabInstance>()
|
|
.register_type::<WeaponSpawn>()
|
|
.register_type::<TriggerVolume>()
|
|
.register_type::<PostProcessVolumeDesc>()
|
|
.register_type::<PostProcessVolumeOverrides>()
|
|
.register_type::<TeamSpawn>()
|
|
.register_type::<ObjectiveMarker>()
|
|
.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<LevelObject>, Without<AuthoringComponentStates>),
|
|
>,
|
|
) {
|
|
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::<AuthoringComponentStates>(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::<InspectorOrder>(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]
|
|
);
|
|
}
|
|
}
|