Blacksite/crates/shared/src/lib.rs

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]
);
}
}