//! Runtime adapter for versioned Blacksite `.scn.ron` assets. use std::io; use bevy::asset::{io::Reader, AssetApp, AssetLoader, LoadContext}; use bevy::ecs::reflect::AppTypeRegistry; use bevy::prelude::*; use bevy::reflect::{TypePath, TypeRegistryArc}; use bevy::world_serialization::{serde::WorldDeserializer, DynamicWorld}; use serde::de::DeserializeSeed; pub(super) struct SchemaWorldAssetPlugin; impl Plugin for SchemaWorldAssetPlugin { fn build(&self, app: &mut App) { // Register after Bevy's stock loader so versioned Blacksite scenes use // this adapter while retaining the standard DynamicWorld asset type. app.init_asset_loader::(); } } #[derive(Debug, TypePath)] struct SchemaWorldAssetLoader { type_registry: TypeRegistryArc, } impl FromWorld for SchemaWorldAssetLoader { fn from_world(world: &mut World) -> Self { Self { type_registry: world.resource::().0.clone(), } } } impl AssetLoader for SchemaWorldAssetLoader { type Asset = DynamicWorld; type Settings = (); type Error = io::Error; async fn load( &self, reader: &mut dyn Reader, _settings: &Self::Settings, load_context: &mut LoadContext<'_>, ) -> Result { let mut bytes = Vec::new(); reader.read_to_end(&mut bytes).await?; let text = std::str::from_utf8(&bytes).map_err(invalid_scene)?; let body = normalized_world_body(text)?; let mut deserializer = ron::de::Deserializer::from_str(&body).map_err(invalid_scene)?; let world_deserializer = WorldDeserializer { type_registry: &self.type_registry.read(), load_from_path: load_context, }; world_deserializer .deserialize(&mut deserializer) .map_err(|error| invalid_scene(deserializer.span_error(error))) } fn extensions(&self) -> &[&str] { &["scn.ron"] } } fn normalized_world_body(text: &str) -> Result { scene::validate_level_text(text).map_err(invalid_scene)?; let migrated = scene::migrate_scene_text(text).map_err(invalid_scene)?; scene::strip_schema_version(&migrated).map_err(invalid_scene) } fn invalid_scene(error: impl std::fmt::Display) -> io::Error { io::Error::new(io::ErrorKind::InvalidData, error.to_string()) } #[cfg(test)] mod tests { use super::normalized_world_body; use super::SchemaWorldAssetPlugin; use bevy::asset::{AssetPlugin, AssetServer, Assets, LoadState}; use bevy::prelude::*; use bevy::world_serialization::{DynamicWorld, WorldSerializationPlugin}; #[test] fn versioned_world_assets_are_validated_and_unwrapped_for_bevy() { let body = normalized_world_body( r#"(schema_version: 2, resources: {}, entities: { 1: (components: { "shared::components::LevelObject": () }), })"#, ) .unwrap(); assert!(body.starts_with("(resources:")); assert!(!body.contains("schema_version")); } #[test] fn invalid_hierarchy_is_rejected_before_world_deserialization() { let error = normalized_world_body( r#"(schema_version: 2, resources: {}, entities: { 1: (components: { "bevy_ecs::hierarchy::ChildOf": (9) }), })"#, ) .unwrap_err(); assert!(error.to_string().contains("missing hierarchy parent `9`")); } #[test] fn asset_server_loads_the_committed_versioned_prefab_fixture() { let assets = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets"); let mut app = App::new(); app.add_plugins(MinimalPlugins) .add_plugins(AssetPlugin { file_path: assets.to_string_lossy().into_owned(), ..default() }) .add_plugins(WorldSerializationPlugin) .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .add_plugins(SchemaWorldAssetPlugin); let handle: Handle = app .world() .resource::() .load("prefabs/example_variant.scn.ron"); for _ in 0..200 { app.update(); if app .world() .resource::>() .get(&handle) .is_some() { return; } if matches!( app.world() .resource::() .load_state(handle.id()), LoadState::Failed(_) ) { break; } std::thread::sleep(std::time::Duration::from_millis(5)); } panic!( "versioned prefab fixture did not load: {:?}", app.world() .resource::() .load_state(handle.id()) ); } }