use crate::prelude::*;
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, BufWriter};
use std::path::Path;
#[cfg(feature = "audio")]
use std::str::FromStr;
use freecs::Entity;
use crate::ecs::audio::components::AudioSource;
#[cfg(feature = "audio")]
use crate::ecs::audio::systems::load_sound_from_cursor;
use crate::ecs::bounding_volume::components::{BoundingVolume, OrientedBoundingBox};
use crate::ecs::generational_registry::registry_entry_by_name;
use crate::ecs::lines::components::Lines;
use crate::ecs::material::resources::material_registry_insert;
use crate::ecs::mesh::components::InstancedMesh;
use crate::ecs::prefab::{import_gltf_from_path, spawn_prefab};
use crate::ecs::world::Vec3;
use crate::ecs::world::commands::RenderCommand;
use crate::ecs::world::components::{
CastsShadow, GlobalTransform, LocalTransformDirty, MaterialRef, Name, Parent, RenderMesh,
Visibility,
};
use crate::ecs::world::{
ANIMATION_PLAYER, AUDIO_SOURCE, BOUNDING_VOLUME, CAMERA, CASTS_SHADOW, DECAL, GLOBAL_TRANSFORM,
GRASS_INTERACTOR, GRASS_REGION, INSTANCED_MESH, LIGHT, LINES, LOCAL_TRANSFORM,
LOCAL_TRANSFORM_DIRTY, MATERIAL_REF, NAME, NAVMESH_AGENT, PARENT, PARTICLE_EMITTER,
RENDER_LAYER, RENDER_MESH, TEXT, VISIBILITY, World,
};
use crate::render::wgpu::texture_cache::texture_cache_add_reference;
use super::asset_uuid::AssetUuid;
use super::audio::ScenePrefabInstance;
use super::components::{Scene, SceneComponents, SceneEntity, SceneHdrSkybox};
use super::lighting::SceneLight;
use super::material::SceneMaterial;
use super::registry::AssetRegistry;
#[derive(Debug, thiserror::Error)]
pub enum SceneError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Binary error: {0}")]
Binary(#[from] bincode::Error),
#[error("Asset not found: {0}")]
AssetNotFound(String),
#[error("Invalid scene structure")]
InvalidStructure,
#[error("Cyclic parent reference detected")]
CyclicReference,
}
pub fn save_scene_json(scene: &mut Scene, path: &Path) -> Result<(), SceneError> {
scene.compute_spawn_order();
let mut value = serde_json::to_value(&*scene)?;
sanitize_numeric_nulls(&mut value);
let file = File::create(path)?;
let writer = BufWriter::new(file);
serde_json::to_writer_pretty(writer, &value)?;
Ok(())
}
fn sanitize_numeric_nulls(value: &mut serde_json::Value) {
match value {
serde_json::Value::Array(items) => {
let any_number = items.iter().any(|item| item.is_number());
let only_numeric_or_null = items.iter().all(|item| item.is_number() || item.is_null());
if any_number && only_numeric_or_null {
for item in items.iter_mut() {
if item.is_null() {
*item = serde_json::Value::from(0.0);
}
}
} else {
for item in items.iter_mut() {
sanitize_numeric_nulls(item);
}
}
}
serde_json::Value::Object(map) => {
for inner in map.values_mut() {
sanitize_numeric_nulls(inner);
}
}
_ => {}
}
}
pub fn load_scene_json(path: &Path) -> Result<Scene, SceneError> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut scene: Scene = serde_json::from_reader(reader)?;
scene.rebuild_uuid_index();
Ok(scene)
}
pub fn save_scene_binary(scene: &mut Scene, path: &Path) -> Result<(), SceneError> {
let compressed = save_scene_binary_to_bytes(scene)?;
std::fs::write(path, compressed)?;
Ok(())
}
pub fn save_scene_binary_to_bytes(scene: &mut Scene) -> Result<Vec<u8>, SceneError> {
scene.compute_spawn_order();
let data = bincode::serialize(scene)?;
Ok(lz4_flex::compress_prepend_size(&data))
}
pub fn load_scene_binary(path: &Path) -> Result<Scene, SceneError> {
let compressed = std::fs::read(path)?;
let data = lz4_flex::decompress_size_prepended(&compressed)
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
let mut scene: Scene = bincode::deserialize(&data)?;
scene.rebuild_uuid_index();
Ok(scene)
}
pub fn load_scene_binary_from_bytes(bytes: &[u8]) -> Result<Scene, SceneError> {
let data = lz4_flex::decompress_size_prepended(bytes)
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
let mut scene: Scene = bincode::deserialize(&data)?;
scene.rebuild_uuid_index();
Ok(scene)
}
pub fn save_scene(scene: &mut Scene, path: &Path) -> Result<(), SceneError> {
match path.extension().and_then(|extension| extension.to_str()) {
Some("json") => save_scene_json(scene, path),
_ => save_scene_binary(scene, path),
}
}
pub fn load_scene(path: &Path) -> Result<Scene, SceneError> {
match path.extension().and_then(|extension| extension.to_str()) {
Some("json") => load_scene_json(path),
_ => load_scene_binary(path),
}
}
pub struct SpawnSceneResult {
pub uuid_to_entity: HashMap<AssetUuid, Entity>,
pub root_entities: Vec<Entity>,
pub warnings: Vec<String>,
pub entity_to_source_uuid: HashMap<Entity, AssetUuid>,
}
pub fn spawn_scene(
world: &mut World,
scene: &Scene,
asset_registry: Option<&AssetRegistry>,
) -> Result<SpawnSceneResult, SceneError> {
spawn_scene_with_deserializer(world, scene, asset_registry, None)
}
pub fn spawn_scene_with_deserializer(
world: &mut World,
scene: &Scene,
asset_registry: Option<&AssetRegistry>,
deserializer: Option<&mut dyn crate::ecs::scene::SceneDeserializer>,
) -> Result<SpawnSceneResult, SceneError> {
let mut uuid_to_entity: HashMap<AssetUuid, Entity> = HashMap::new();
let mut root_entities: Vec<Entity> = Vec::new();
let mut warnings: Vec<String> = Vec::new();
world.resources.graphics.atmosphere = scene.atmosphere;
apply_scene_settings(world, &scene.settings);
if scene.next_guid > world.resources.entities.next_guid {
world.resources.entities.next_guid = scene.next_guid;
}
if let Some(hdr_skybox) = &scene.hdr_skybox {
match hdr_skybox {
SceneHdrSkybox::Embedded { data } => {
queue_render_command(
world,
RenderCommand::LoadHdrSkybox {
hdr_data: data.clone(),
},
);
}
SceneHdrSkybox::Reference { path } => {
queue_render_command(
world,
RenderCommand::LoadHdrSkyboxFromPath {
path: std::path::PathBuf::from(path),
},
);
}
SceneHdrSkybox::Asset { uuid } => {
if let Some(registry) = asset_registry {
if let Some(path) = asset_registry_resolve_uuid(registry, *uuid) {
queue_render_command(world, RenderCommand::LoadHdrSkyboxFromPath { path });
} else {
warnings.push(format!("HDR skybox asset not found: {}", uuid));
}
} else {
warnings.push(format!(
"Asset registry required to resolve HDR skybox UUID: {}",
uuid
));
}
}
}
}
for embedded_texture in scene.embedded_textures.values() {
if let Some((rgba_data, width, height)) = embedded_texture.to_rgba() {
crate::ecs::loading::queue_decoded_texture(
world,
embedded_texture.name.clone(),
rgba_data,
width,
height,
embedded_texture.usage,
embedded_texture.sampler,
);
if let crate::ecs::scene::audio::EmbeddedTextureData::Png { png_data } =
&embedded_texture.data
{
world.resources.assets.texture_sources.insert(
embedded_texture.name.clone(),
crate::ecs::asset_state::TextureSourceBytes {
data: crate::ecs::asset_state::TextureSourceData::Png(png_data.clone()),
usage: embedded_texture.usage,
sampler: embedded_texture.sampler,
},
);
}
}
}
for embedded_mesh in scene.embedded_meshes.values() {
crate::ecs::prefab::resources::mesh_cache_insert(
&mut world.resources.assets.mesh_cache,
embedded_mesh.name.clone(),
embedded_mesh.mesh.clone(),
);
}
let spawn_order = if !scene.spawn_order.is_empty() {
scene.spawn_order.clone()
} else {
compute_spawn_order(&scene.entities)?
};
for scene_entity_uuid in spawn_order {
let scene_entity = scene
.find_entity(scene_entity_uuid)
.ok_or(SceneError::InvalidStructure)?;
let parent_entity = scene_entity
.parent
.and_then(|parent_uuid| uuid_to_entity.get(&parent_uuid).copied());
let entity = spawn_scene_entity(
world,
scene,
scene_entity,
parent_entity,
asset_registry,
&mut warnings,
);
if scene_entity.parent.is_none() {
root_entities.push(entity);
}
uuid_to_entity.insert(scene_entity.uuid, entity);
}
#[cfg(feature = "navmesh")]
if let Some(navmesh) = &scene.navmesh {
world.resources.navmesh = navmesh.to_navmesh_world();
}
#[cfg(feature = "physics")]
super::commands_physics::spawn_scene_joints(
world,
&scene.joints,
&uuid_to_entity,
&mut warnings,
);
resolve_animation_targets(world, scene, &uuid_to_entity);
if let Some(deserializer) = deserializer {
deserializer.populate_world(world, &uuid_to_entity, scene);
}
let mut entity_to_source_uuid: HashMap<Entity, AssetUuid> = HashMap::new();
for scene_entity in &scene.entities {
if let (Some(source_uuid), Some(&entity)) = (
scene_entity.source_entity_uuid,
uuid_to_entity.get(&scene_entity.uuid),
) {
entity_to_source_uuid.insert(entity, source_uuid);
}
}
Ok(SpawnSceneResult {
uuid_to_entity,
root_entities,
warnings,
entity_to_source_uuid,
})
}
fn resolve_animation_targets(
world: &mut World,
scene: &Scene,
uuid_to_entity: &HashMap<AssetUuid, Entity>,
) {
for scene_entity in &scene.entities {
let Some(scene_player) = scene_entity.components.animation_player.as_ref() else {
continue;
};
if scene_player.bone_name_to_node.is_empty() && scene_player.node_index_to_node.is_empty() {
continue;
}
let Some(&entity) = uuid_to_entity.get(&scene_entity.uuid) else {
continue;
};
let Some(player) = world.core.get_animation_player_mut(entity) else {
continue;
};
for (bone_name, bone_uuid) in &scene_player.bone_name_to_node {
if let Some(&bone_entity) = uuid_to_entity.get(bone_uuid) {
player
.bone_name_to_entity
.insert(bone_name.clone(), bone_entity);
}
}
for (&node_index, node_uuid) in &scene_player.node_index_to_node {
if let Some(&node_entity) = uuid_to_entity.get(node_uuid) {
player.node_index_to_entity.insert(node_index, node_entity);
}
}
}
}
fn compute_spawn_order(entities: &[SceneEntity]) -> Result<Vec<AssetUuid>, SceneError> {
let uuid_set: std::collections::HashSet<AssetUuid> = entities.iter().map(|e| e.uuid).collect();
let mut result: Vec<AssetUuid> = Vec::with_capacity(entities.len());
let mut visited: std::collections::HashSet<AssetUuid> = std::collections::HashSet::new();
let mut in_stack: std::collections::HashSet<AssetUuid> = std::collections::HashSet::new();
let entity_map: HashMap<AssetUuid, &SceneEntity> =
entities.iter().map(|e| (e.uuid, e)).collect();
fn visit(
uuid: AssetUuid,
entity_map: &HashMap<AssetUuid, &SceneEntity>,
uuid_set: &std::collections::HashSet<AssetUuid>,
visited: &mut std::collections::HashSet<AssetUuid>,
in_stack: &mut std::collections::HashSet<AssetUuid>,
result: &mut Vec<AssetUuid>,
) -> Result<(), SceneError> {
if visited.contains(&uuid) {
return Ok(());
}
if in_stack.contains(&uuid) {
return Err(SceneError::CyclicReference);
}
in_stack.insert(uuid);
if let Some(entity) = entity_map.get(&uuid)
&& let Some(parent_uuid) = entity.parent
&& uuid_set.contains(&parent_uuid)
{
visit(parent_uuid, entity_map, uuid_set, visited, in_stack, result)?;
}
in_stack.remove(&uuid);
visited.insert(uuid);
result.push(uuid);
Ok(())
}
for entity in entities {
visit(
entity.uuid,
&entity_map,
&uuid_set,
&mut visited,
&mut in_stack,
&mut result,
)?;
}
Ok(result)
}
fn spawn_scene_entity(
world: &mut World,
scene: &Scene,
scene_entity: &SceneEntity,
parent_entity: Option<Entity>,
asset_registry: Option<&AssetRegistry>,
warnings: &mut Vec<String>,
) -> Entity {
let mut component_mask = LOCAL_TRANSFORM | GLOBAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY;
if scene_entity.name.is_some() {
component_mask |= NAME;
}
if parent_entity.is_some() {
component_mask |= PARENT;
}
if scene_entity.components.visible {
component_mask |= VISIBILITY;
}
let entity = spawn_entities(world, component_mask, 1)[0];
world
.core
.set_local_transform(entity, scene_entity.transform);
world
.core
.set_global_transform(entity, GlobalTransform::default());
world
.core
.set_local_transform_dirty(entity, LocalTransformDirty);
if let Some(name) = &scene_entity.name {
world.core.set_name(entity, Name(name.clone()));
world.resources.entities.names.insert(name.clone(), entity);
}
if !scene_entity.components.tags.is_empty() {
world
.resources
.entities
.tags
.insert(entity, scene_entity.components.tags.clone());
}
if let Some(guid) = scene_entity.guid {
crate::ecs::entity_registry::assign_guid(world, entity, crate::ecs::primitives::Guid(guid));
}
if let Some(parent) = parent_entity {
world.core.set_parent(entity, Parent(Some(parent)));
world
.resources
.transform_state
.children_cache
.entry(parent)
.or_default()
.push(entity);
}
if scene_entity.components.visible {
world
.core
.set_visibility(entity, Visibility { visible: true });
}
apply_scene_components(
world,
scene,
entity,
&scene_entity.components,
asset_registry,
warnings,
);
entity
}
fn apply_scene_components(
world: &mut World,
scene: &Scene,
entity: Entity,
components: &SceneComponents,
asset_registry: Option<&AssetRegistry>,
warnings: &mut Vec<String>,
) {
if let Some(mesh) = &components.mesh {
apply_mesh_component(world, entity, mesh, asset_registry, warnings);
}
if let Some(instanced_mesh) = &components.instanced_mesh {
apply_instanced_mesh_component(world, entity, instanced_mesh, asset_registry, warnings);
}
if !components.lines.is_empty() {
world.core.add_components(entity, LINES);
world
.core
.set_lines(entity, Lines::new(components.lines.clone()));
}
if let Some(light) = &components.light {
world.core.add_components(entity, LIGHT);
world.core.set_light(entity, light.to_light());
}
if let Some(camera) = &components.camera {
world.core.add_components(entity, CAMERA);
world.core.set_camera(entity, camera.to_camera());
}
#[cfg(feature = "physics")]
if let Some(physics) = &components.physics {
super::commands_physics::apply_physics_component(world, entity, physics);
}
if let Some(audio) = &components.audio {
apply_audio_component(world, scene, entity, audio, asset_registry, warnings);
}
if let Some(prefab) = &components.prefab {
apply_prefab_component(world, entity, prefab, asset_registry, warnings);
}
if let Some(bounding_volume) = &components.bounding_volume {
world.core.add_components(entity, BOUNDING_VOLUME);
world.core.set_bounding_volume(entity, *bounding_volume);
}
if let Some(animation_player) = &components.animation_player {
world.core.add_components(entity, ANIMATION_PLAYER);
let player = if animation_player.clip_refs.is_empty() {
animation_player.to_animation_player()
} else {
animation_player.to_animation_player_with_resolved_clips(asset_registry, warnings)
};
world.core.set_animation_player(entity, player);
}
if components.casts_shadow {
world.core.add_components(entity, CASTS_SHADOW);
world.core.set_casts_shadow(entity, CastsShadow);
}
if let Some(scene_emitter) = &components.particle_emitter {
world.core.add_components(entity, PARTICLE_EMITTER);
world
.core
.set_particle_emitter(entity, scene_emitter.to_particle_emitter());
}
if let Some(decal) = &components.decal {
world.core.add_components(entity, DECAL);
world.core.set_decal(entity, decal.clone());
}
if let Some(grass_region) = &components.grass_region {
world.core.add_components(entity, GRASS_REGION);
world.core.set_grass_region(entity, grass_region.clone());
}
if let Some(grass_interactor) = &components.grass_interactor {
world.core.add_components(entity, GRASS_INTERACTOR);
world
.core
.set_grass_interactor(entity, grass_interactor.clone());
}
if let Some(render_layer) = &components.render_layer {
world.core.add_components(entity, RENDER_LAYER);
world.core.set_render_layer(entity, *render_layer);
}
if let Some(text) = &components.text {
let mut text = text.clone();
if let Some(content) = components.text_content.as_deref() {
text.text_index = world.resources.text.cache.add_text(content);
}
text.dirty = true;
world.core.add_components(entity, TEXT);
world.core.set_text(entity, text);
}
#[cfg(feature = "physics")]
if let Some(scene_cc) = &components.character_controller {
super::commands_physics::apply_character_controller(world, entity, scene_cc);
}
if let Some(navmesh_agent) = &components.navmesh_agent {
world.core.add_components(entity, NAVMESH_AGENT);
world.core.set_navmesh_agent(entity, navmesh_agent.clone());
}
}
fn apply_scene_settings(world: &mut World, settings: &super::settings::SceneSettings) {
#[cfg(feature = "physics")]
super::commands_physics::apply_physics_settings(world, settings);
let graphics = &mut world.resources.graphics;
graphics.ambient_light = settings.ambient_light;
graphics.clear_color = settings.clear_color;
graphics.fog = settings.fog;
graphics.bloom_enabled = settings.bloom_enabled;
graphics.bloom_intensity = settings.bloom_intensity;
graphics.bloom_threshold = settings.bloom_threshold;
graphics.color_grading = settings.color_grading;
graphics.depth_of_field = settings.depth_of_field;
graphics.ssao_enabled = settings.ssao_enabled;
graphics.ssao_radius = settings.ssao_radius;
graphics.ssao_bias = settings.ssao_bias;
graphics.ssao_intensity = settings.ssao_intensity;
graphics.ssao_sample_count = settings.ssao_sample_count;
graphics.ssgi_enabled = settings.ssgi_enabled;
graphics.ssgi_radius = settings.ssgi_radius;
graphics.ssgi_intensity = settings.ssgi_intensity;
graphics.ssgi_max_steps = settings.ssgi_max_steps;
graphics.ssr_enabled = settings.ssr_enabled;
graphics.ssr_max_steps = settings.ssr_max_steps;
graphics.ssr_thickness = settings.ssr_thickness;
graphics.ssr_max_distance = settings.ssr_max_distance;
graphics.ssr_stride = settings.ssr_stride;
graphics.ssr_fade_start = settings.ssr_fade_start;
graphics.ssr_fade_end = settings.ssr_fade_end;
graphics.ssr_intensity = settings.ssr_intensity;
graphics.vertex_snap = settings.vertex_snap;
graphics.affine_texture_mapping = settings.affine_texture_mapping;
if let Some(hour) = settings.day_night_hour {
graphics.day_night.hour = hour;
}
}
pub fn capture_scene_settings(world: &World) -> super::settings::SceneSettings {
let graphics = &world.resources.graphics;
#[cfg(feature = "physics")]
let (gravity, physics_timestep, physics_max_substeps) =
super::commands_physics::capture_physics_settings(world);
#[cfg(not(feature = "physics"))]
let (gravity, physics_timestep, physics_max_substeps) = ([0.0, -9.81, 0.0], 1.0 / 60.0, 4);
let day_night_hour = if matches!(
graphics.atmosphere,
crate::ecs::graphics::resources::Atmosphere::DayNight
) {
Some(graphics.day_night.hour)
} else {
None
};
super::settings::SceneSettings {
gravity,
physics_timestep,
physics_max_substeps,
ambient_light: graphics.ambient_light,
clear_color: graphics.clear_color,
fog: graphics.fog,
bloom_enabled: graphics.bloom_enabled,
bloom_intensity: graphics.bloom_intensity,
bloom_threshold: graphics.bloom_threshold,
color_grading: graphics.color_grading,
depth_of_field: graphics.depth_of_field,
ssao_enabled: graphics.ssao_enabled,
ssao_radius: graphics.ssao_radius,
ssao_bias: graphics.ssao_bias,
ssao_intensity: graphics.ssao_intensity,
ssao_sample_count: graphics.ssao_sample_count,
ssgi_enabled: graphics.ssgi_enabled,
ssgi_radius: graphics.ssgi_radius,
ssgi_intensity: graphics.ssgi_intensity,
ssgi_max_steps: graphics.ssgi_max_steps,
ssr_enabled: graphics.ssr_enabled,
ssr_max_steps: graphics.ssr_max_steps,
ssr_thickness: graphics.ssr_thickness,
ssr_max_distance: graphics.ssr_max_distance,
ssr_stride: graphics.ssr_stride,
ssr_fade_start: graphics.ssr_fade_start,
ssr_fade_end: graphics.ssr_fade_end,
ssr_intensity: graphics.ssr_intensity,
vertex_snap: graphics.vertex_snap,
affine_texture_mapping: graphics.affine_texture_mapping,
day_night_hour,
}
}
fn apply_mesh_component(
world: &mut World,
entity: Entity,
mesh: &super::components::SceneMesh,
asset_registry: Option<&AssetRegistry>,
warnings: &mut Vec<String>,
) {
let mesh_name = if let Some(name) = &mesh.mesh_name {
name.clone()
} else if let Some(uuid) = mesh.mesh_uuid {
if let Some(registry) = asset_registry {
if let Some(entry) = asset_registry_get_entry(registry, uuid) {
entry.name.clone().unwrap_or_else(|| uuid.to_string())
} else {
warnings.push(format!("Mesh asset not found: {}", uuid));
return;
}
} else {
uuid.to_string()
}
} else {
warnings.push("Mesh component has neither UUID nor name".to_string());
return;
};
world.core.add_components(
entity,
RENDER_MESH | MATERIAL_REF | CASTS_SHADOW | BOUNDING_VOLUME,
);
let render_mesh = RenderMesh::new(mesh_name.clone());
world
.core
.set_bounding_volume(entity, BoundingVolume::from_mesh_type(&mesh_name));
if let Some(&index) = world
.resources
.assets
.mesh_cache
.registry
.name_to_index
.get(&render_mesh.name)
{
registry_add_reference(&mut world.resources.assets.mesh_cache.registry, index);
}
world.core.set_render_mesh(entity, render_mesh);
world.resources.mesh_render_state.mark_entity_added(entity);
let material_name = if let Some(scene_material) = &mesh.material {
let mat = scene_material.to_material();
let name = format!("Scene_{}_{}", mesh_name, entity.id);
material_registry_insert(
&mut world.resources.assets.material_registry,
name.clone(),
mat,
);
name
} else {
"Default".to_string()
};
if let Some(&index) = world
.resources
.assets
.material_registry
.registry
.name_to_index
.get(&material_name)
{
registry_add_reference(
&mut world.resources.assets.material_registry.registry,
index,
);
}
let texture_names: Vec<String> = registry_entry_by_name(
&world.resources.assets.material_registry.registry,
&material_name,
)
.map(|mat| mat.texture_names().map(str::to_string).collect())
.unwrap_or_default();
for texture in &texture_names {
texture_cache_add_reference(&mut world.resources.texture_cache, texture);
}
world
.core
.set_material_ref(entity, MaterialRef::new(material_name));
world.core.set_casts_shadow(entity, CastsShadow);
}
fn compute_instanced_mesh_bounding_volume(
mesh_name: &str,
instances: &[super::mesh::SceneMeshInstance],
) -> BoundingVolume {
if instances.is_empty() {
return BoundingVolume::from_mesh_type(mesh_name);
}
let base_bv = BoundingVolume::from_mesh_type(mesh_name);
let base_obb = &base_bv.obb;
let mut min_corner = Vec3::new(f32::MAX, f32::MAX, f32::MAX);
let mut max_corner = Vec3::new(f32::MIN, f32::MIN, f32::MIN);
for instance in instances {
let local_transform = instance.to_local_transform();
let instance_matrix = nalgebra_glm::translation(&local_transform.translation)
* nalgebra_glm::quat_to_mat4(&local_transform.rotation)
* nalgebra_glm::scaling(&local_transform.scale);
let transformed_obb = base_obb.transform(&instance_matrix);
let corners = transformed_obb.get_corners();
for corner in &corners {
min_corner.x = min_corner.x.min(corner.x);
min_corner.y = min_corner.y.min(corner.y);
min_corner.z = min_corner.z.min(corner.z);
max_corner.x = max_corner.x.max(corner.x);
max_corner.y = max_corner.y.max(corner.y);
max_corner.z = max_corner.z.max(corner.z);
}
}
let combined_obb = OrientedBoundingBox::from_aabb(min_corner, max_corner);
let sphere_radius = nalgebra_glm::length(&combined_obb.half_extents);
BoundingVolume::new(combined_obb, sphere_radius)
}
fn apply_instanced_mesh_component(
world: &mut World,
entity: Entity,
instanced_mesh: &super::mesh::SceneInstancedMesh,
asset_registry: Option<&AssetRegistry>,
warnings: &mut Vec<String>,
) {
use crate::ecs::mesh::components::{InstanceCustomData, InstanceTransform};
let mesh_name = if let Some(name) = &instanced_mesh.mesh_name {
name.clone()
} else if let Some(uuid) = instanced_mesh.mesh_uuid {
if let Some(registry) = asset_registry {
if let Some(entry) = asset_registry_get_entry(registry, uuid) {
entry.name.clone().unwrap_or_else(|| uuid.to_string())
} else {
warnings.push(format!("Instanced mesh asset not found: {}", uuid));
return;
}
} else {
uuid.to_string()
}
} else {
warnings.push("Instanced mesh component has neither UUID nor name".to_string());
return;
};
world.core.add_components(
entity,
INSTANCED_MESH | MATERIAL_REF | CASTS_SHADOW | BOUNDING_VOLUME,
);
let bounding_volume =
compute_instanced_mesh_bounding_volume(&mesh_name, &instanced_mesh.instances);
world.core.set_bounding_volume(entity, bounding_volume);
let transforms: Vec<InstanceTransform> = instanced_mesh
.instances
.iter()
.map(|i| {
let local = i.to_local_transform();
InstanceTransform::new(local.translation, local.rotation, local.scale)
})
.collect();
let mut component = InstancedMesh::with_instances(&mesh_name, transforms);
for (index, instance) in instanced_mesh.instances.iter().enumerate() {
if let Some(color) = instance.color {
component.custom_data[index] = InstanceCustomData { tint: color };
}
}
world.core.set_instanced_mesh(entity, component);
let material_name = if let Some(scene_material) = &instanced_mesh.material {
let mat = scene_material.to_material();
let name = format!("SceneInstanced_{}_{}", mesh_name, entity.id);
material_registry_insert(
&mut world.resources.assets.material_registry,
name.clone(),
mat,
);
name
} else {
"Default".to_string()
};
if let Some(&index) = world
.resources
.assets
.material_registry
.registry
.name_to_index
.get(&material_name)
{
registry_add_reference(
&mut world.resources.assets.material_registry.registry,
index,
);
}
world
.core
.set_material_ref(entity, MaterialRef::new(material_name));
world.core.set_casts_shadow(entity, CastsShadow);
}
fn apply_audio_component(
world: &mut World,
scene: &Scene,
entity: Entity,
audio: &super::audio::SceneAudioSource,
_asset_registry: Option<&AssetRegistry>,
warnings: &mut Vec<String>,
) {
world.core.add_components(entity, AUDIO_SOURCE);
let audio_ref = if let Some(name) = &audio.audio_name {
name.clone()
} else if let Some(uuid) = audio.audio_uuid {
uuid.to_string()
} else {
warnings.push("Audio component has neither UUID nor name".to_string());
return;
};
#[cfg(feature = "audio")]
{
let resolved_uuid = audio
.audio_uuid
.or_else(|| AssetUuid::from_str(&audio_ref).ok());
match resolved_uuid.and_then(|uuid| scene.embedded_audio.get(&uuid)) {
Some(embedded_audio) => {
if let Ok(sound_data) = load_sound_from_cursor(embedded_audio.data.clone()) {
audio_engine_load_sound(
&mut world.resources.audio,
audio_ref.clone(),
sound_data,
);
}
}
None => {
warnings.push(format!("Audio '{}' not found in scene", audio_ref));
}
}
}
#[cfg(not(feature = "audio"))]
{
let _ = scene;
warnings.push("Audio feature is not enabled - audio source will not play".to_string());
}
let mut reverb_zones = audio.reverb_zones.clone();
if reverb_zones.is_empty() && audio.reverb {
reverb_zones.push(("default".to_string(), 0.0));
}
world.core.set_audio_source(
entity,
AudioSource {
audio_ref: Some(audio_ref),
volume: audio.volume,
looping: audio.looping,
playing: audio.playing,
spatial: audio.spatial,
bus: audio.bus,
min_distance: audio.min_distance,
max_distance: audio.max_distance,
reverb_zones,
random_clips: audio.random_clips.clone(),
random_pick: audio.random_pick,
playback_rate: 1.0,
},
);
}
fn apply_prefab_component(
world: &mut World,
entity: Entity,
prefab_instance: &ScenePrefabInstance,
asset_registry: Option<&AssetRegistry>,
warnings: &mut Vec<String>,
) {
attach_prefab_source(world, entity, prefab_instance);
let is_nsprefab = prefab_instance.prefab_path.as_deref().is_some_and(|path| {
std::path::Path::new(path)
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| extension.eq_ignore_ascii_case("nsprefab"))
});
if is_nsprefab || prefab_instance.prefab_path.is_none() {
return;
}
let prefab_path = if let Some(uuid) = prefab_instance.prefab_uuid {
if let Some(registry) = asset_registry {
asset_registry_resolve_uuid(registry, uuid)
} else {
warnings.push(format!(
"Asset registry required to resolve prefab UUID: {}",
uuid
));
return;
}
} else if let Some(path) = &prefab_instance.prefab_path {
Some(std::path::PathBuf::from(path))
} else {
warnings.push("Prefab instance has neither UUID nor path".to_string());
return;
};
if let Some(path) = prefab_path {
match import_gltf_from_path(&path) {
Ok(mut result) => {
crate::ecs::loading::queue_gltf_load(world, &mut result);
if let Some(prefab) = result.prefabs.into_iter().next() {
let transform = world
.core
.get_local_transform(entity)
.copied()
.unwrap_or_default();
let prefab_root = spawn_prefab(world, &prefab, transform.translation);
world.core.add_components(prefab_root, PARENT);
world.core.set_parent(prefab_root, Parent(Some(entity)));
world
.resources
.transform_state
.children_cache
.entry(entity)
.or_default()
.push(prefab_root);
} else {
warnings.push(format!("No prefabs found in file '{}'", path.display()));
}
}
Err(error) => {
warnings.push(format!(
"Failed to load prefab from '{}': {}",
path.display(),
error
));
}
}
}
}
fn attach_prefab_source(world: &mut World, entity: Entity, prefab_instance: &ScenePrefabInstance) {
use crate::ecs::prefab::components::PrefabSource;
world
.core
.add_components(entity, crate::ecs::world::PREFAB_SOURCE);
world.core.set_prefab_source(
entity,
PrefabSource {
prefab_name: prefab_instance.prefab_name.clone().unwrap_or_default(),
source_path: prefab_instance.prefab_path.clone(),
source_uuid: prefab_instance.prefab_uuid,
},
);
}
pub fn entity_to_scene_entity(
world: &World,
entity: Entity,
uuid: AssetUuid,
parent_uuid: Option<AssetUuid>,
) -> SceneEntity {
entity_to_scene_entity_with_uuids(world, entity, uuid, parent_uuid, &HashMap::new())
}
pub fn entity_to_scene_entity_with_uuids(
world: &World,
entity: Entity,
uuid: AssetUuid,
parent_uuid: Option<AssetUuid>,
entity_to_uuid: &HashMap<Entity, AssetUuid>,
) -> SceneEntity {
let transform = world
.core
.get_local_transform(entity)
.copied()
.unwrap_or_default();
let name = world.core.get_name(entity).map(|n| n.0.clone());
let mut scene_entity = SceneEntity {
uuid,
parent: parent_uuid,
name,
transform,
layer: None,
chunk_id: None,
guid: world.core.get_guid(entity).map(|guid| guid.0),
source_entity_uuid: None,
components: SceneComponents::default(),
};
populate_scene_entity_components(world, entity, &mut scene_entity);
populate_animation_target_uuids(world, entity, &mut scene_entity, entity_to_uuid);
scene_entity
}
fn populate_animation_target_uuids(
world: &World,
entity: Entity,
scene_entity: &mut SceneEntity,
entity_to_uuid: &HashMap<Entity, AssetUuid>,
) {
let Some(player) = world.core.get_animation_player(entity) else {
return;
};
if player.bone_name_to_entity.is_empty() && player.node_index_to_entity.is_empty() {
return;
}
let Some(scene_player) = scene_entity.components.animation_player.as_mut() else {
return;
};
for (bone_name, bone_entity) in &player.bone_name_to_entity {
if let Some(&bone_uuid) = entity_to_uuid.get(bone_entity) {
scene_player
.bone_name_to_node
.insert(bone_name.clone(), bone_uuid);
}
}
for (&node_index, node_entity) in &player.node_index_to_entity {
if let Some(&node_uuid) = entity_to_uuid.get(node_entity) {
scene_player
.node_index_to_node
.insert(node_index, node_uuid);
}
}
}
fn populate_scene_entity_components(world: &World, entity: Entity, scene_entity: &mut SceneEntity) {
if let Some(tags) = world.resources.entities.tags.get(&entity) {
scene_entity.components.tags = tags.clone();
}
if let Some(render_mesh) = world.core.get_render_mesh(entity) {
let material = world
.core
.get_material_ref(entity)
.and_then(|mat_ref| {
registry_entry_by_name(
&world.resources.assets.material_registry.registry,
&mat_ref.name,
)
})
.map(SceneMaterial::from);
scene_entity.components.mesh = Some(super::components::SceneMesh {
mesh_uuid: None,
mesh_name: Some(render_mesh.name.clone()),
material,
});
}
if let Some(instanced_mesh) = world.core.get_instanced_mesh(entity) {
let material = world
.core
.get_material_ref(entity)
.and_then(|mat_ref| {
registry_entry_by_name(
&world.resources.assets.material_registry.registry,
&mat_ref.name,
)
})
.map(SceneMaterial::from);
let instances: Vec<super::mesh::SceneMeshInstance> = instanced_mesh
.instances
.iter()
.enumerate()
.map(|(index, transform)| {
let tint = instanced_mesh
.custom_data
.get(index)
.map(|cd| cd.tint)
.filter(|t| *t != [1.0, 1.0, 1.0, 1.0]);
super::mesh::SceneMeshInstance {
translation: [
transform.translation.x,
transform.translation.y,
transform.translation.z,
],
rotation: [
transform.rotation.i,
transform.rotation.j,
transform.rotation.k,
transform.rotation.w,
],
scale: [transform.scale.x, transform.scale.y, transform.scale.z],
color: tint,
}
})
.collect();
scene_entity.components.instanced_mesh = Some(super::mesh::SceneInstancedMesh {
mesh_uuid: None,
mesh_name: Some(instanced_mesh.mesh_name.clone()),
instances,
material,
});
}
if let Some(light) = world.core.get_light(entity) {
scene_entity.components.light = Some(SceneLight::from(light));
}
if let Some(camera) = world.core.get_camera(entity) {
scene_entity.components.camera = Some(super::lighting::SceneCamera::from(camera));
}
#[cfg(feature = "physics")]
super::commands_physics::export_entity_physics(world, entity, &mut scene_entity.components);
if let Some(audio_source) = world.core.get_audio_source(entity) {
let reverb_legacy = audio_source
.reverb_zones
.iter()
.any(|(name, _)| name == "default");
scene_entity.components.audio = Some(super::audio::SceneAudioSource {
audio_uuid: None,
audio_name: audio_source.audio_ref.clone(),
volume: audio_source.volume,
looping: audio_source.looping,
playing: audio_source.playing,
spatial: audio_source.spatial,
reverb: reverb_legacy,
bus: audio_source.bus,
min_distance: audio_source.min_distance,
max_distance: audio_source.max_distance,
reverb_zones: audio_source.reverb_zones.clone(),
random_clips: audio_source.random_clips.clone(),
random_pick: audio_source.random_pick,
});
}
if let Some(bounding_volume) = world.core.get_bounding_volume(entity) {
scene_entity.components.bounding_volume = Some(*bounding_volume);
}
if let Some(animation_player) = world.core.get_animation_player(entity) {
scene_entity.components.animation_player = Some(
super::animation::SceneAnimationPlayer::from(animation_player),
);
}
scene_entity.components.casts_shadow = world.core.get_casts_shadow(entity).is_some();
if let Some(visibility) = world.core.get_visibility(entity) {
scene_entity.components.visible = visibility.visible;
}
if let Some(emitter) = world.core.get_particle_emitter(entity) {
scene_entity.components.particle_emitter =
Some(super::particles::SceneParticleEmitter::from(emitter));
}
if let Some(decal) = world.core.get_decal(entity) {
scene_entity.components.decal = Some(decal.clone());
}
if let Some(grass_region) = world.core.get_grass_region(entity) {
scene_entity.components.grass_region = Some(grass_region.clone());
}
if let Some(grass_interactor) = world.core.get_grass_interactor(entity) {
scene_entity.components.grass_interactor = Some(grass_interactor.clone());
}
if let Some(render_layer) = world.core.get_render_layer(entity) {
scene_entity.components.render_layer = Some(*render_layer);
}
if let Some(text) = world.core.get_text(entity) {
scene_entity.components.text = Some(text.clone());
scene_entity.components.text_content = world
.resources
.text
.cache
.get_text(text.text_index)
.map(str::to_string);
}
if let Some(navmesh_agent) = world.core.get_navmesh_agent(entity) {
scene_entity.components.navmesh_agent = Some(navmesh_agent.clone());
}
if let Some(prefab_source) = world.core.get_prefab_source(entity) {
scene_entity.components.prefab = Some(ScenePrefabInstance {
prefab_uuid: prefab_source.source_uuid,
prefab_path: prefab_source.source_path.clone(),
prefab_name: if prefab_source.prefab_name.is_empty() {
None
} else {
Some(prefab_source.prefab_name.clone())
},
});
}
}
pub fn world_to_scene(world: &World, name: impl Into<String>) -> Scene {
world_to_scene_with_uuids(world, name, &HashMap::new())
}
pub fn world_to_scene_with_serializer(
world: &World,
name: impl Into<String>,
serializer: Option<&dyn crate::ecs::scene::SceneSerializer>,
) -> Scene {
world_to_scene_with_uuids_and_serializer(world, name, &HashMap::new(), serializer)
}
pub fn subtree_to_scene(world: &World, root: Entity, name: impl Into<String>) -> Scene {
let mut scene = Scene::new(name);
let mut entities = vec![root];
entities.extend(crate::ecs::transform::queries::query_descendants(
world, root,
));
let mut entity_to_uuid: HashMap<Entity, AssetUuid> = HashMap::new();
for entity in &entities {
entity_to_uuid.insert(*entity, AssetUuid::random());
}
for (index, entity) in entities.iter().copied().enumerate() {
let uuid = entity_to_uuid[&entity];
let parent_uuid = if index == 0 {
None
} else {
world
.core
.get_parent(entity)
.and_then(|parent| parent.0)
.and_then(|parent_entity| entity_to_uuid.get(&parent_entity).copied())
};
let scene_entity =
entity_to_scene_entity_with_uuids(world, entity, uuid, parent_uuid, &entity_to_uuid);
scene.add_entity(scene_entity);
}
embed_referenced_meshes(world, &mut scene);
embed_referenced_textures(world, &mut scene);
capture_animation_targets(world, &mut scene, &entity_to_uuid);
scene
}
pub fn spawn_prefab_scene(
world: &mut World,
prefab_scene: &Scene,
parent: Option<Entity>,
) -> Result<SpawnSceneResult, SceneError> {
let mut remap: HashMap<AssetUuid, AssetUuid> = HashMap::new();
let mut remapped_scene = prefab_scene.clone();
for scene_entity in &mut remapped_scene.entities {
let new_uuid = AssetUuid::random();
remap.insert(scene_entity.uuid, new_uuid);
scene_entity.uuid = new_uuid;
}
for scene_entity in &mut remapped_scene.entities {
if let Some(parent_uuid) = scene_entity.parent
&& let Some(&new_parent) = remap.get(&parent_uuid)
{
scene_entity.parent = Some(new_parent);
}
}
for scene_entity in &mut remapped_scene.entities {
if let Some(player) = scene_entity.components.animation_player.as_mut() {
let original_bones = std::mem::take(&mut player.bone_name_to_node);
for (bone_name, old_uuid) in original_bones {
if let Some(&new_uuid) = remap.get(&old_uuid) {
player.bone_name_to_node.insert(bone_name, new_uuid);
}
}
let original_nodes = std::mem::take(&mut player.node_index_to_node);
for (node_index, old_uuid) in original_nodes {
if let Some(&new_uuid) = remap.get(&old_uuid) {
player.node_index_to_node.insert(node_index, new_uuid);
}
}
}
}
remapped_scene.rebuild_uuid_index();
remapped_scene.compute_spawn_order();
let mut result = spawn_scene(world, &remapped_scene, None)?;
for (source_uuid, new_uuid) in &remap {
if let Some(entity) = result.uuid_to_entity.get(new_uuid) {
result.entity_to_source_uuid.insert(*entity, *source_uuid);
}
}
if let Some(parent) = parent {
for &root_entity in &result.root_entities {
world
.core
.add_components(root_entity, crate::ecs::world::PARENT);
world.core.set_parent(
root_entity,
crate::ecs::world::components::Parent(Some(parent)),
);
world
.resources
.transform_state
.children_cache
.entry(parent)
.or_default()
.push(root_entity);
}
}
Ok(result)
}
pub fn world_to_scene_with_uuids(
world: &World,
name: impl Into<String>,
stable_uuids: &HashMap<Entity, AssetUuid>,
) -> Scene {
world_to_scene_with_uuids_and_serializer(world, name, stable_uuids, None)
}
pub fn world_to_scene_with_uuids_and_serializer(
world: &World,
name: impl Into<String>,
stable_uuids: &HashMap<Entity, AssetUuid>,
serializer: Option<&dyn crate::ecs::scene::SceneSerializer>,
) -> Scene {
let mut scene = Scene::new(name);
scene.atmosphere = world.resources.graphics.atmosphere;
scene.settings = capture_scene_settings(world);
scene.next_guid = world.resources.entities.next_guid;
let mut entity_to_uuid: HashMap<Entity, AssetUuid> = HashMap::new();
let mut entities: Vec<Entity> = Vec::new();
world
.core
.query()
.with(LOCAL_TRANSFORM)
.iter(|entity, _, _| {
let uuid = stable_uuids
.get(&entity)
.copied()
.unwrap_or_else(AssetUuid::random);
entity_to_uuid.insert(entity, uuid);
entities.push(entity);
});
for entity in &entities {
let entity = *entity;
let uuid = entity_to_uuid[&entity];
let parent_uuid = world
.core
.get_parent(entity)
.and_then(|p| p.0)
.and_then(|parent_entity| entity_to_uuid.get(&parent_entity).copied());
let scene_entity =
entity_to_scene_entity_with_uuids(world, entity, uuid, parent_uuid, &entity_to_uuid);
scene.add_entity(scene_entity);
}
embed_referenced_meshes(world, &mut scene);
embed_referenced_textures(world, &mut scene);
capture_animation_targets(world, &mut scene, &entity_to_uuid);
#[cfg(feature = "physics")]
super::commands_physics::export_scene_joints(world, &entity_to_uuid, &mut scene);
#[cfg(feature = "navmesh")]
if !world.resources.navmesh.triangles.is_empty() {
scene.navmesh = Some(super::navigation::SceneNavMesh::from_navmesh_world(
&world.resources.navmesh,
));
}
if let Some(serializer) = serializer {
serializer.populate_scene(world, &entity_to_uuid, &mut scene);
}
scene
}
pub fn embed_referenced_meshes(world: &World, scene: &mut Scene) {
use crate::ecs::generational_registry::registry_entry_by_name;
let mut name_to_uuid: HashMap<String, AssetUuid> = HashMap::new();
for scene_entity in &mut scene.entities {
if let Some(mesh) = scene_entity.components.mesh.as_mut()
&& let Some(mesh_name) = mesh.mesh_name.clone()
{
let uuid = *name_to_uuid
.entry(mesh_name.clone())
.or_insert_with(AssetUuid::random);
mesh.mesh_uuid = Some(uuid);
}
if let Some(instanced) = scene_entity.components.instanced_mesh.as_mut()
&& let Some(mesh_name) = instanced.mesh_name.clone()
{
let uuid = *name_to_uuid
.entry(mesh_name.clone())
.or_insert_with(AssetUuid::random);
instanced.mesh_uuid = Some(uuid);
}
}
for (mesh_name, uuid) in name_to_uuid {
if scene.embedded_meshes.contains_key(&uuid) {
continue;
}
if let Some(mesh) =
registry_entry_by_name(&world.resources.assets.mesh_cache.registry, &mesh_name)
{
scene.embedded_meshes.insert(
uuid,
super::mesh::EmbeddedMesh::new(mesh_name, mesh.clone()),
);
}
}
}
fn capture_animation_targets(
world: &World,
scene: &mut Scene,
entity_to_uuid: &HashMap<Entity, AssetUuid>,
) {
let uuid_to_scene_index: HashMap<AssetUuid, usize> = scene
.entities
.iter()
.enumerate()
.map(|(index, entity)| (entity.uuid, index))
.collect();
for (entity, uuid) in entity_to_uuid {
let Some(player) = world.core.get_animation_player(*entity) else {
continue;
};
if player.bone_name_to_entity.is_empty() && player.node_index_to_entity.is_empty() {
continue;
}
let Some(&scene_index) = uuid_to_scene_index.get(uuid) else {
continue;
};
let Some(scene_player) = scene.entities[scene_index]
.components
.animation_player
.as_mut()
else {
continue;
};
for (bone_name, bone_entity) in &player.bone_name_to_entity {
if let Some(&bone_uuid) = entity_to_uuid.get(bone_entity) {
scene_player
.bone_name_to_node
.insert(bone_name.clone(), bone_uuid);
}
}
for (&node_index, node_entity) in &player.node_index_to_entity {
if let Some(&node_uuid) = entity_to_uuid.get(node_entity) {
scene_player
.node_index_to_node
.insert(node_index, node_uuid);
}
}
}
}
pub fn embed_referenced_textures(world: &World, scene: &mut Scene) {
use crate::ecs::asset_state::TextureSourceData;
use crate::ecs::scene::audio::EmbeddedTexture;
let mut texture_names: std::collections::HashSet<String> = std::collections::HashSet::new();
for scene_entity in &scene.entities {
if let Some(mesh) = scene_entity.components.mesh.as_ref()
&& let Some(material) = mesh.material.as_ref()
{
texture_names.extend(material.texture_names().map(|name| name.to_string()));
}
if let Some(instanced) = scene_entity.components.instanced_mesh.as_ref()
&& let Some(material) = instanced.material.as_ref()
{
texture_names.extend(material.texture_names().map(|name| name.to_string()));
}
}
for texture_name in texture_names {
let Some(source) = world.resources.assets.texture_sources.get(&texture_name) else {
continue;
};
let base = match &source.data {
TextureSourceData::Png(bytes) => {
EmbeddedTexture::from_png(&texture_name, bytes.clone())
}
TextureSourceData::Rgba {
rgba,
width,
height,
} => EmbeddedTexture::from_rgba(&texture_name, rgba.clone(), *width, *height),
};
let embedded = base.with_usage(source.usage).with_sampler(source.sampler);
scene
.embedded_textures
.insert(AssetUuid::random(), embedded);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ecs::asset_state::TextureSourceBytes;
use crate::ecs::material::components::Material;
use crate::ecs::material::resources::material_registry_insert;
use crate::ecs::world::commands::spawn_mesh_at;
use crate::ecs::world::components::MaterialRef;
use crate::ecs::world::{MATERIAL_REF, World};
#[test]
fn binary_round_trip_preserves_referenced_textures() {
let mut source_world = World::default();
let entity = spawn_mesh_at(
&mut source_world,
"Cube",
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 1.0, 1.0),
);
source_world.resources.assets.texture_sources.insert(
"test_diffuse".to_string(),
TextureSourceBytes {
data: crate::ecs::asset_state::TextureSourceData::Rgba {
rgba: vec![255u8; 4],
width: 1,
height: 1,
},
usage: crate::render::wgpu::texture_cache::TextureUsage::Color,
sampler: crate::render::wgpu::texture_cache::SamplerSettings::DEFAULT,
},
);
let material = Material {
base_texture: Some("test_diffuse".to_string()),
..Default::default()
};
material_registry_insert(
&mut source_world.resources.assets.material_registry,
"TestMaterial".to_string(),
material,
);
source_world.core.add_components(entity, MATERIAL_REF);
source_world
.core
.set_material_ref(entity, MaterialRef::new("TestMaterial"));
let mut scene = world_to_scene(&source_world, "TextureRoundTrip");
assert_eq!(
scene.embedded_textures.len(),
1,
"world_to_scene must embed referenced textures"
);
let embedded = scene
.embedded_textures
.values()
.next()
.expect("texture must be embedded");
assert_eq!(embedded.name, "test_diffuse");
let encoded = save_scene_binary_to_bytes(&mut scene).expect("encode");
let decoded = load_scene_binary_from_bytes(&encoded).expect("decode");
let decoded_texture = decoded
.embedded_textures
.values()
.next()
.expect("texture must survive bincode round-trip");
assert_eq!(decoded_texture.name, "test_diffuse");
assert!(decoded_texture.to_rgba().is_some());
let mut destination_world = World::default();
spawn_scene(&mut destination_world, &decoded, None).expect("spawn");
assert!(
destination_world
.resources
.texture_cache
.registry
.name_to_index
.contains_key("test_diffuse"),
"embedded texture must land in destination texture_cache under its original name"
);
}
#[test]
fn binary_round_trip_preserves_meshes_and_entities() {
let mut source_world = World::default();
let entity = spawn_mesh_at(
&mut source_world,
"Cube",
Vec3::new(1.0, 2.0, 3.0),
Vec3::new(1.5, 1.5, 1.5),
);
source_world.resources.entities.tags.insert(
entity,
vec!["round_trip_marker".to_string(), "player_spawn".to_string()],
);
let mut scene = world_to_scene(&source_world, "RoundTrip");
assert!(
!scene.embedded_meshes.is_empty(),
"world_to_scene must embed referenced meshes"
);
let embedded_cube_uuid = scene
.embedded_meshes
.iter()
.find(|(_, mesh)| mesh.name == "Cube")
.map(|(uuid, _)| *uuid)
.expect("Cube mesh must be embedded under its original name");
assert!(
scene
.entities
.iter()
.any(
|entity| entity.components.mesh.as_ref().is_some_and(|mesh| {
mesh.mesh_name.as_deref() == Some("Cube")
&& mesh.mesh_uuid == Some(embedded_cube_uuid)
})
)
);
let encoded = save_scene_binary_to_bytes(&mut scene).expect("encode scene");
let decoded = load_scene_binary_from_bytes(&encoded).expect("decode scene");
assert_eq!(decoded.embedded_meshes.len(), 1);
let decoded_cube = decoded
.embedded_meshes
.get(&embedded_cube_uuid)
.expect("Cube mesh must survive bincode round-trip");
assert_eq!(decoded_cube.name, "Cube");
assert!(!decoded_cube.mesh.vertices.is_empty());
assert!(!decoded_cube.mesh.indices.is_empty());
let mut destination_world = World::default();
let result = spawn_scene(&mut destination_world, &decoded, None).expect("spawn scene");
assert_eq!(result.uuid_to_entity.len(), 1);
assert!(
destination_world
.resources
.assets
.mesh_cache
.registry
.name_to_index
.contains_key("Cube"),
"embedded mesh must land in destination mesh_cache before entities spawn"
);
let spawned_entity = *result
.uuid_to_entity
.values()
.next()
.expect("spawned entity");
let render_mesh = destination_world
.core
.get_render_mesh(spawned_entity)
.expect("entity must have RenderMesh after spawn_scene");
assert_eq!(render_mesh.name, "Cube");
let transform = destination_world
.core
.get_local_transform(spawned_entity)
.expect("entity must have LocalTransform");
assert_eq!(transform.translation, Vec3::new(1.0, 2.0, 3.0));
assert_eq!(transform.scale, Vec3::new(1.5, 1.5, 1.5));
let tags = destination_world
.resources
.entities
.tags
.get(&spawned_entity)
.expect("tags must survive round-trip");
assert!(tags.contains(&"round_trip_marker".to_string()));
assert!(tags.contains(&"player_spawn".to_string()));
}
#[test]
fn typed_components_round_trip_through_scene() {
use crate::ecs::scene::{
Persistence, SceneDeserializer, SceneSerializer, TypedComponent, TypedPayload,
TypedResource,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct TestPayload {
label: String,
amplitude: f32,
offsets: [f32; 3],
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct TestResource {
score: u32,
note: String,
}
struct CaptureSerializer {
entity_payload: HashMap<Entity, TestPayload>,
resource_payload: TestResource,
}
impl SceneSerializer for CaptureSerializer {
fn populate_scene(
&self,
_world: &World,
entity_uuids: &HashMap<Entity, AssetUuid>,
scene: &mut Scene,
) {
for (entity, payload) in &self.entity_payload {
let Some(uuid) = entity_uuids.get(entity) else {
continue;
};
let Some(scene_entity) = scene.find_entity_mut(*uuid) else {
continue;
};
scene_entity
.components
.game_components
.push(TypedComponent {
name: "test_payload".to_string(),
data: TypedPayload(
serde_json::to_value(payload).expect("serialize entity payload"),
),
persistence: Persistence::Both,
field_overrides: Vec::new(),
});
}
scene.game_resources.push(TypedResource {
name: "test_resource".to_string(),
data: TypedPayload(
serde_json::to_value(&self.resource_payload)
.expect("serialize resource payload"),
),
persistence: Persistence::Runtime,
});
}
}
struct CaptureDeserializer {
entity_payloads: HashMap<Entity, TestPayload>,
resource_payload: Option<TestResource>,
}
impl SceneDeserializer for CaptureDeserializer {
fn populate_world(
&mut self,
_world: &mut World,
uuid_to_entity: &HashMap<AssetUuid, Entity>,
scene: &Scene,
) {
for scene_entity in &scene.entities {
let Some(&entity) = uuid_to_entity.get(&scene_entity.uuid) else {
continue;
};
for typed in &scene_entity.components.game_components {
if typed.name == "test_payload" {
let payload: TestPayload = serde_json::from_value(typed.data.0.clone())
.expect("deserialize entity payload");
self.entity_payloads.insert(entity, payload);
}
}
}
for resource in &scene.game_resources {
if resource.name == "test_resource" {
self.resource_payload = Some(
serde_json::from_value(resource.data.0.clone())
.expect("deserialize resource payload"),
);
}
}
}
}
let payload = TestPayload {
label: "north_orb".to_string(),
amplitude: 0.42,
offsets: [1.0, -2.5, 3.75],
};
let resource = TestResource {
score: 7,
note: "checkpoint".to_string(),
};
for binary in [false, true] {
let mut source_world = World::default();
let entity = spawn_mesh_at(
&mut source_world,
"Cube",
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 1.0, 1.0),
);
let mut entity_payload = HashMap::new();
entity_payload.insert(entity, payload.clone());
let serializer = CaptureSerializer {
entity_payload,
resource_payload: resource.clone(),
};
let mut scene =
world_to_scene_with_serializer(&source_world, "TypedRoundTrip", Some(&serializer));
let decoded = if binary {
let encoded = save_scene_binary_to_bytes(&mut scene).expect("encode scene");
load_scene_binary_from_bytes(&encoded).expect("decode scene")
} else {
let encoded = serde_json::to_vec(&scene).expect("encode json");
let mut parsed: Scene = serde_json::from_slice(&encoded).expect("decode json");
parsed.rebuild_uuid_index();
parsed
};
let mut destination_world = World::default();
let mut deserializer = CaptureDeserializer {
entity_payloads: HashMap::new(),
resource_payload: None,
};
let result = spawn_scene_with_deserializer(
&mut destination_world,
&decoded,
None,
Some(&mut deserializer),
)
.expect("spawn scene");
let spawned_entity = *result
.uuid_to_entity
.values()
.next()
.expect("spawned entity");
assert_eq!(
deserializer.entity_payloads.get(&spawned_entity),
Some(&payload),
"entity payload must survive {} round-trip",
if binary { "binary" } else { "json" }
);
assert_eq!(
deserializer.resource_payload.as_ref(),
Some(&resource),
"resource payload must survive {} round-trip",
if binary { "binary" } else { "json" }
);
}
}
#[test]
fn component_descriptors_register_and_lookup() {
use crate::ecs::scene::{
ComponentDescriptor, FieldDescriptor, FieldKind, Persistence, component_descriptor,
register_component_descriptor,
};
let mut world = World::default();
register_component_descriptor(
&mut world,
ComponentDescriptor {
name: "door".to_string(),
display_name: Some("Door".to_string()),
persistence: Persistence::Both,
fields: vec![
FieldDescriptor {
name: "required_key".to_string(),
kind: FieldKind::Bool,
display_name: Some("Requires Key".to_string()),
},
FieldDescriptor {
name: "open_y".to_string(),
kind: FieldKind::F32,
display_name: None,
},
],
},
);
let descriptor =
component_descriptor(&world, "door").expect("door descriptor must be findable");
assert_eq!(descriptor.name, "door");
assert_eq!(descriptor.persistence, Persistence::Both);
assert_eq!(descriptor.fields.len(), 2);
assert_eq!(descriptor.fields[0].name, "required_key");
assert_eq!(descriptor.fields[0].kind, FieldKind::Bool);
assert_eq!(descriptor.fields[1].name, "open_y");
assert_eq!(descriptor.fields[1].kind, FieldKind::F32);
assert!(component_descriptor(&world, "unknown").is_none());
}
#[test]
fn field_overrides_layer_onto_authored_payload() {
use crate::ecs::prefab::components::FieldOverride;
use crate::ecs::scene::{Persistence, TypedComponent, TypedPayload, apply_field_overrides};
let authored = serde_json::json!({
"kind": "Lever",
"credit_reward": 15,
"activated": false,
"lever_progress": 0.0,
});
let runtime_overrides = vec![
FieldOverride {
field_name: "activated".to_string(),
value: TypedPayload(serde_json::Value::Bool(true)),
},
FieldOverride {
field_name: "lever_progress".to_string(),
value: TypedPayload(serde_json::json!(0.75)),
},
];
let component = TypedComponent {
name: "world_prop".to_string(),
data: TypedPayload(authored.clone()),
persistence: Persistence::Both,
field_overrides: runtime_overrides,
};
for binary in [false, true] {
let decoded: TypedComponent = if binary {
let encoded = bincode::serialize(&component).expect("encode component");
bincode::deserialize(&encoded).expect("decode component")
} else {
let encoded = serde_json::to_string(&component).expect("encode component");
serde_json::from_str(&encoded).expect("decode component")
};
let merged = apply_field_overrides(&decoded.data.0, &decoded.field_overrides);
assert_eq!(merged["kind"], serde_json::json!("Lever"));
assert_eq!(merged["credit_reward"], serde_json::json!(15));
assert_eq!(merged["activated"], serde_json::json!(true));
assert_eq!(merged["lever_progress"], serde_json::json!(0.75));
}
}
#[test]
fn guid_round_trips_through_scene() {
use crate::ecs::entity_registry::{ensure_guid, find_entity_by_guid};
let mut source_world = World::default();
let entity = spawn_mesh_at(
&mut source_world,
"Cube",
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 1.0, 1.0),
);
let original_guid = ensure_guid(&mut source_world, entity);
let original_next = source_world.resources.entities.next_guid;
assert_eq!(original_guid.0, original_next);
let mut scene = world_to_scene(&source_world, "GuidRoundTrip");
assert_eq!(scene.next_guid, original_next);
let scene_entity = scene
.entities
.iter()
.find(|scene_entity| scene_entity.guid == Some(original_guid.0))
.expect("guid must be written into scene");
let _ = scene_entity;
let encoded = save_scene_binary_to_bytes(&mut scene).expect("encode scene");
let decoded = load_scene_binary_from_bytes(&encoded).expect("decode scene");
let mut destination_world = World::default();
let result = spawn_scene(&mut destination_world, &decoded, None).expect("spawn scene");
let spawned_entity = *result
.uuid_to_entity
.values()
.next()
.expect("spawned entity");
let restored = destination_world
.core
.get_guid(spawned_entity)
.copied()
.expect("guid component must round-trip");
assert_eq!(restored, original_guid);
assert_eq!(
destination_world.resources.entities.next_guid,
original_next
);
assert_eq!(
find_entity_by_guid(&destination_world, original_guid),
Some(spawned_entity)
);
}
#[test]
fn text_content_round_trips_through_scene() {
use crate::ecs::text::components::Text;
use crate::ecs::world::{
GLOBAL_TRANSFORM, LOCAL_TRANSFORM, LOCAL_TRANSFORM_DIRTY, TEXT, VISIBILITY,
};
let mut source_world = World::default();
let content = "Welcome to Nightshade";
let text_index = source_world.resources.text.cache.add_text(content);
let entity = spawn_entities(
&mut source_world,
TEXT | LOCAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | GLOBAL_TRANSFORM | VISIBILITY,
1,
)[0];
source_world.core.set_text(
entity,
Text {
text_index,
billboard: true,
..Default::default()
},
);
let mut scene = world_to_scene(&source_world, "TextRoundTrip");
let encoded = save_scene_binary_to_bytes(&mut scene).expect("encode scene");
let decoded = load_scene_binary_from_bytes(&encoded).expect("decode scene");
let mut destination_world = World::default();
let result = spawn_scene(&mut destination_world, &decoded, None).expect("spawn scene");
let spawned_entity = *result
.uuid_to_entity
.values()
.next()
.expect("spawned entity");
let restored_text = destination_world
.core
.get_text(spawned_entity)
.expect("text component must survive round-trip");
let restored_content = destination_world
.resources
.text
.cache
.get_text(restored_text.text_index)
.expect("text cache must hold the restored content");
assert_eq!(restored_content, content);
}
#[test]
fn prefab_source_link_survives_map_round_trip() {
use crate::ecs::prefab::components::PrefabSource;
use crate::ecs::world::PREFAB_SOURCE;
let mut source_world = World::default();
let entity = spawn_mesh_at(
&mut source_world,
"Cube",
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 1.0, 1.0),
);
let prefab_uuid = AssetUuid::random();
source_world.core.add_components(entity, PREFAB_SOURCE);
source_world.core.set_prefab_source(
entity,
PrefabSource {
prefab_name: "Dancer".to_string(),
source_path: Some("prefabs/dancer.nsprefab".to_string()),
source_uuid: Some(prefab_uuid),
},
);
let mut scene = world_to_scene(&source_world, "MapWithPrefab");
let scene_entity = scene
.entities
.iter()
.find(|entity| entity.components.prefab.is_some())
.expect("entity with prefab source must produce ScenePrefabInstance");
let scene_prefab = scene_entity.components.prefab.as_ref().unwrap();
assert_eq!(scene_prefab.prefab_uuid, Some(prefab_uuid));
assert_eq!(
scene_prefab.prefab_path.as_deref(),
Some("prefabs/dancer.nsprefab")
);
let encoded = save_scene_binary_to_bytes(&mut scene).expect("encode");
let decoded = load_scene_binary_from_bytes(&encoded).expect("decode");
let mut destination_world = World::default();
let result = spawn_scene(&mut destination_world, &decoded, None).expect("spawn");
let spawned_entity = *result
.uuid_to_entity
.values()
.next()
.expect("entity must spawn");
let attached = destination_world
.core
.get_prefab_source(spawned_entity)
.expect("PrefabSource must be attached after .nsprefab round-trip");
assert_eq!(attached.source_uuid, Some(prefab_uuid));
assert_eq!(
attached.source_path.as_deref(),
Some("prefabs/dancer.nsprefab")
);
assert_eq!(attached.prefab_name, "Dancer");
}
#[test]
fn prefab_subtree_round_trip_spawns_multiple_instances() {
let mut source_world = World::default();
let root = spawn_mesh_at(
&mut source_world,
"Cube",
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 1.0, 1.0),
);
let mut prefab_scene = subtree_to_scene(&source_world, root, "test_prefab");
assert_eq!(prefab_scene.entities.len(), 1);
assert!(!prefab_scene.embedded_meshes.is_empty());
let encoded = save_scene_binary_to_bytes(&mut prefab_scene).expect("encode");
let decoded_prefab = load_scene_binary_from_bytes(&encoded).expect("decode");
let mut destination_world = World::default();
let first = spawn_prefab_scene(&mut destination_world, &decoded_prefab, None)
.expect("first instance");
let second = spawn_prefab_scene(&mut destination_world, &decoded_prefab, None)
.expect("second instance");
for first_uuid in first.uuid_to_entity.keys() {
assert!(
!second.uuid_to_entity.contains_key(first_uuid),
"prefab instances must mint fresh uuids"
);
}
assert_eq!(first.root_entities.len(), 1);
assert_eq!(second.root_entities.len(), 1);
assert_ne!(first.root_entities[0], second.root_entities[0]);
assert!(
destination_world
.resources
.assets
.mesh_cache
.registry
.name_to_index
.contains_key("Cube"),
);
}
#[test]
fn nested_prefab_source_survives_subtree_round_trip() {
use crate::ecs::prefab::components::PrefabSource;
use crate::ecs::world::PARENT;
use crate::ecs::world::PREFAB_SOURCE;
use crate::ecs::world::components::Parent;
let mut source_world = World::default();
let outer_root = spawn_mesh_at(
&mut source_world,
"Cube",
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 1.0, 1.0),
);
let inner = spawn_mesh_at(
&mut source_world,
"Cube",
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 1.0, 1.0),
);
source_world.core.add_components(inner, PARENT);
source_world
.core
.set_parent(inner, Parent(Some(outer_root)));
let inner_prefab_uuid = AssetUuid::random();
source_world.core.add_components(inner, PREFAB_SOURCE);
source_world.core.set_prefab_source(
inner,
PrefabSource {
prefab_name: "Door".to_string(),
source_path: Some("prefabs/door.nsprefab".to_string()),
source_uuid: Some(inner_prefab_uuid),
},
);
let mut prefab_scene = subtree_to_scene(&source_world, outer_root, "outer_prefab");
let encoded = save_scene_binary_to_bytes(&mut prefab_scene).expect("encode");
let decoded = load_scene_binary_from_bytes(&encoded).expect("decode");
let mut destination_world = World::default();
let result =
spawn_prefab_scene(&mut destination_world, &decoded, None).expect("spawn outer");
let inner_descendants: Vec<Entity> = destination_world
.core
.query_entities(PREFAB_SOURCE)
.collect();
let inner_match = inner_descendants
.iter()
.find_map(|entity| {
destination_world
.core
.get_prefab_source(*entity)
.filter(|source| source.source_uuid == Some(inner_prefab_uuid))
.map(|source| (*entity, source.clone()))
})
.expect("nested PrefabSource must survive subtree round trip");
assert!(
!result.root_entities.contains(&inner_match.0),
"inner instance should remain a descendant, not become a root"
);
assert_eq!(inner_match.1.prefab_name, "Door");
assert_eq!(
inner_match.1.source_path.as_deref(),
Some("prefabs/door.nsprefab")
);
}
}