161 lines
5.2 KiB
Rust
161 lines
5.2 KiB
Rust
//! 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::<SchemaWorldAssetLoader>();
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, TypePath)]
|
|
struct SchemaWorldAssetLoader {
|
|
type_registry: TypeRegistryArc,
|
|
}
|
|
|
|
impl FromWorld for SchemaWorldAssetLoader {
|
|
fn from_world(world: &mut World) -> Self {
|
|
Self {
|
|
type_registry: world.resource::<AppTypeRegistry>().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<Self::Asset, Self::Error> {
|
|
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<String, io::Error> {
|
|
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::<Name>()
|
|
.register_type::<Transform>()
|
|
.register_type::<shared::LevelObject>()
|
|
.register_type::<shared::ActorId>()
|
|
.register_type::<shared::ActorKind>()
|
|
.register_type::<shared::EditorVisibility>()
|
|
.register_type::<shared::PrefabInstance>()
|
|
.register_type::<shared::PrefabRef>()
|
|
.add_plugins(SchemaWorldAssetPlugin);
|
|
|
|
let handle: Handle<DynamicWorld> = app
|
|
.world()
|
|
.resource::<AssetServer>()
|
|
.load("prefabs/example_variant.scn.ron");
|
|
for _ in 0..200 {
|
|
app.update();
|
|
if app
|
|
.world()
|
|
.resource::<Assets<DynamicWorld>>()
|
|
.get(&handle)
|
|
.is_some()
|
|
{
|
|
return;
|
|
}
|
|
if matches!(
|
|
app.world()
|
|
.resource::<AssetServer>()
|
|
.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::<AssetServer>()
|
|
.load_state(handle.id())
|
|
);
|
|
}
|
|
}
|