Blacksite/crates/game/src/schema_world_loader.rs
Rbanh 0553a85220
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run
Build production-ready editor authoring workflows
2026-07-11 12:41:04 -04:00

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