use crate::ecs::EditorWorld;
use crate::project::ProjectState;
use crate::scene_writeback::{sync_hdr_skybox, sync_settings};
use crate::systems::loading;
#[cfg(target_arch = "wasm32")]
use nightshade::ecs::scene::commands::save_scene_binary_to_bytes;
use nightshade::ecs::scene::commands::{load_scene_binary_from_bytes, world_to_scene_with_uuids};
use nightshade::ecs::scene::{Scene, SceneHdrSkybox, spawn_scene};
use nightshade::filesystem::{FileFilter, PendingFileLoad};
use nightshade::prelude::*;
const MAP_EXTENSIONS: &[&str] = &["nsmap"];
const PREFAB_EXTENSIONS: &[&str] = &["nsprefab"];
pub enum PendingLoadKind {
Map,
Prefab,
}
pub struct PendingLoad {
pub kind: PendingLoadKind,
pub handle: PendingFileLoad,
}
#[derive(Default)]
pub struct ProjectIoState {
pub pending_load: Option<PendingLoad>,
}
pub fn new_project(editor_world: &mut EditorWorld, world: &mut World) {
despawn_known_world_entities(editor_world, world);
editor_world.resources.editor_scene.clear();
editor_world.resources.project = ProjectState::new("Untitled");
editor_world.resources.writeback_state = Default::default();
editor_world.resources.writeback_state.mark_full_resync();
editor_world.resources.undo.clear();
if editor_world.resources.sun.sun_entity.is_none() {
crate::systems::sun::spawn_with_shadows(editor_world, world);
}
sync_settings(&mut editor_world.resources.project, world);
let _ = sync_hdr_skybox;
}
pub fn open_project(editor_world: &mut EditorWorld) {
let filters = [FileFilter {
name: "Nightshade map".to_string(),
extensions: MAP_EXTENSIONS.iter().map(|s| s.to_string()).collect(),
}];
editor_world.resources.project_io.pending_load = Some(PendingLoad {
kind: PendingLoadKind::Map,
handle: nightshade::filesystem::request_file_load(&filters),
});
}
pub fn import_prefab(editor_world: &mut EditorWorld) {
let filters = [FileFilter {
name: "Nightshade prefab".to_string(),
extensions: PREFAB_EXTENSIONS.iter().map(|s| s.to_string()).collect(),
}];
editor_world.resources.project_io.pending_load = Some(PendingLoad {
kind: PendingLoadKind::Prefab,
handle: nightshade::filesystem::request_file_load(&filters),
});
}
pub fn save_selected_as_prefab(editor_world: &mut EditorWorld, world: &mut World) {
let Some(selected) = editor_world.resources.ui.selected_entity else {
tracing::warn!("No entity selected; nothing to save as prefab");
return;
};
let default_name = world
.core
.get_name(selected)
.map(|name| format!("{}.nsprefab", name.0))
.unwrap_or_else(|| "prefab.nsprefab".to_string());
#[cfg(not(target_arch = "wasm32"))]
{
let filters = [FileFilter {
name: "Nightshade prefab".to_string(),
extensions: vec!["nsprefab".to_string()],
}];
let Some(path) = nightshade::filesystem::save_file_dialog(&filters, Some(&default_name))
else {
return;
};
let prefab_name = path
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or("prefab")
.to_string();
let mut scene =
nightshade::ecs::scene::commands::subtree_to_scene(world, selected, prefab_name);
let prior_source = world.core.get_prefab_source(selected);
let preserves_identity = prior_source.and_then(|source| {
source
.source_path
.as_deref()
.map(|prior_path| std::path::Path::new(prior_path) == path.as_path())
}) == Some(true);
scene.header.prefab_id = Some(if preserves_identity {
prior_source
.and_then(|source| source.source_uuid)
.unwrap_or_else(nightshade::ecs::scene::AssetUuid::random)
} else {
nightshade::ecs::scene::AssetUuid::random()
});
match nightshade::ecs::scene::commands::save_scene(&mut scene, &path) {
Ok(()) => {
tracing::info!("Saved prefab to {}", path.display());
let thumbnail_path = path.with_extension("thumb.png");
hide_other_entities_for_thumbnail(editor_world, world, selected);
nightshade::ecs::world::commands::capture_thumbnail_to_path(
world,
thumbnail_path,
256,
);
}
Err(error) => tracing::error!("Failed to save prefab: {error}"),
}
}
#[cfg(target_arch = "wasm32")]
{
use nightshade::ecs::scene::commands::{save_scene_binary_to_bytes, subtree_to_scene};
let filters = [FileFilter {
name: "Nightshade prefab".to_string(),
extensions: vec!["nsprefab".to_string()],
}];
let mut scene = subtree_to_scene(world, selected, "prefab");
let bytes = match save_scene_binary_to_bytes(&mut scene) {
Ok(bytes) => bytes,
Err(error) => {
tracing::error!("Failed to serialize prefab: {error}");
return;
}
};
if let Err(error) = nightshade::filesystem::save_file(&default_name, &bytes, &filters) {
tracing::error!("Failed to save prefab: {error}");
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn hide_other_entities_for_thumbnail(
editor_world: &mut EditorWorld,
world: &mut World,
instance_root: Entity,
) {
use std::collections::HashSet;
let mut instance_subtree: HashSet<Entity> = HashSet::new();
instance_subtree.insert(instance_root);
for descendant in nightshade::ecs::transform::queries::query_descendants(world, instance_root) {
instance_subtree.insert(descendant);
}
let mut hidden: Vec<Entity> = Vec::new();
let candidate_entities: Vec<Entity> = world
.core
.query_entities(nightshade::ecs::world::VISIBILITY)
.collect();
for entity in candidate_entities {
if instance_subtree.contains(&entity) {
continue;
}
let Some(visibility) = world.core.get_visibility(entity) else {
continue;
};
if !visibility.visible {
continue;
}
world
.core
.set_visibility(entity, Visibility { visible: false });
hidden.push(entity);
}
editor_world.resources.loading.pending_thumbnail_restore = hidden;
editor_world
.resources
.loading
.pending_thumbnail_restore_frames = 2;
}
pub fn poll_thumbnail_isolation(editor_world: &mut EditorWorld, world: &mut World) {
let frames = editor_world
.resources
.loading
.pending_thumbnail_restore_frames;
if frames == 0 {
return;
}
let next = frames - 1;
editor_world
.resources
.loading
.pending_thumbnail_restore_frames = next;
if next > 0 {
return;
}
let hidden = std::mem::take(&mut editor_world.resources.loading.pending_thumbnail_restore);
for entity in hidden {
if world.core.get_visibility(entity).is_some() {
world
.core
.set_visibility(entity, Visibility { visible: true });
}
}
}
pub fn save_project(editor_world: &mut EditorWorld, world: &mut World) {
flush_writeback(editor_world, world);
#[cfg(not(target_arch = "wasm32"))]
{
if let Some(path) = editor_world.resources.project.current_path.clone() {
write_project_to_path(editor_world, world, &path);
return;
}
}
save_project_as(editor_world, world);
}
pub fn save_project_as(editor_world: &mut EditorWorld, world: &mut World) {
flush_writeback(editor_world, world);
let default_name = if editor_world.resources.project.scene_name.is_empty() {
"untitled.nsmap".to_string()
} else {
format!("{}.nsmap", editor_world.resources.project.scene_name)
};
#[cfg(not(target_arch = "wasm32"))]
{
let filters = [FileFilter {
name: "Nightshade map".to_string(),
extensions: vec!["nsmap".to_string()],
}];
let Some(path) = nightshade::filesystem::save_file_dialog(&filters, Some(&default_name))
else {
return;
};
if editor_world.resources.project.scene_name.is_empty()
&& let Some(stem) = path.file_stem().and_then(|stem| stem.to_str())
{
editor_world.resources.project.scene_name = stem.to_string();
}
editor_world.resources.project.current_path = Some(path.clone());
write_project_to_path(editor_world, world, &path);
}
#[cfg(target_arch = "wasm32")]
{
let filters = [FileFilter {
name: "Nightshade map".to_string(),
extensions: vec!["nsmap".to_string()],
}];
let bytes = match encode_world_as_binary_scene(editor_world, world) {
Ok(bytes) => bytes,
Err(error) => {
tracing::error!("Failed to serialize map: {error}");
return;
}
};
if let Err(error) = nightshade::filesystem::save_file(&default_name, &bytes, &filters) {
tracing::error!("Failed to save map: {error}");
return;
}
editor_world.resources.project.clear_modified();
}
}
#[cfg(not(target_arch = "wasm32"))]
fn write_project_to_path(
editor_world: &mut EditorWorld,
world: &mut World,
path: &std::path::Path,
) {
let scene_name = editor_world.resources.project.scene_name.clone();
let mut scene = world_to_scene_with_uuids(
world,
scene_name,
&editor_world.resources.editor_scene.entity_to_uuid,
);
stamp_source_entity_uuids(editor_world, &mut scene);
match nightshade::ecs::scene::commands::save_scene(&mut scene, path) {
Ok(()) => {
editor_world.resources.project.scene = scene;
editor_world.resources.project.clear_modified();
tracing::info!("Saved map to {}", path.display());
}
Err(error) => {
tracing::error!("Failed to save map: {error}");
}
}
}
#[cfg(target_arch = "wasm32")]
fn encode_world_as_binary_scene(
editor_world: &EditorWorld,
world: &World,
) -> Result<Vec<u8>, nightshade::ecs::scene::commands::SceneError> {
let scene_name = editor_world.resources.project.scene_name.clone();
let mut scene = world_to_scene_with_uuids(
world,
scene_name,
&editor_world.resources.editor_scene.entity_to_uuid,
);
stamp_source_entity_uuids(editor_world, &mut scene);
save_scene_binary_to_bytes(&mut scene)
}
fn stamp_source_entity_uuids(editor_world: &EditorWorld, scene: &mut Scene) {
let links = &editor_world
.resources
.prefab_instance_links
.entity_to_source;
if links.is_empty() {
return;
}
let uuid_to_entity = &editor_world.resources.editor_scene.uuid_to_entity;
for scene_entity in &mut scene.entities {
if let Some(&entity) = uuid_to_entity.get(&scene_entity.uuid)
&& let Some(source_uuid) = links.get(&entity)
{
scene_entity.source_entity_uuid = Some(*source_uuid);
}
}
}
pub fn load_project_bytes_into_editor(
editor_world: &mut EditorWorld,
world: &mut World,
name: &str,
bytes: &[u8],
) {
apply_loaded_map_bytes(
editor_world,
world,
&nightshade::filesystem::LoadedFile {
name: name.to_string(),
bytes: bytes.to_vec(),
},
);
}
pub fn poll_pending_loads(editor_world: &mut EditorWorld, world: &mut World) {
let Some(pending) = editor_world.resources.project_io.pending_load.take() else {
return;
};
if !pending.handle.is_ready() {
editor_world.resources.project_io.pending_load = Some(pending);
return;
}
let Some(loaded) = pending.handle.take() else {
return;
};
match pending.kind {
PendingLoadKind::Map => apply_loaded_map_bytes(editor_world, world, &loaded),
PendingLoadKind::Prefab => apply_loaded_prefab_bytes(editor_world, world, &loaded),
}
}
pub fn import_prefab_from_path(
editor_world: &mut EditorWorld,
world: &mut World,
path: &std::path::Path,
) {
let Ok(bytes) = std::fs::read(path) else {
tracing::error!("Failed to read prefab '{}'", path.display());
return;
};
let name = path
.file_name()
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_default();
let loaded = nightshade::filesystem::LoadedFile { name, bytes };
apply_loaded_prefab_bytes_with_path(editor_world, world, &loaded, Some(path));
}
fn apply_loaded_prefab_bytes(
editor_world: &mut EditorWorld,
world: &mut World,
loaded: &nightshade::filesystem::LoadedFile,
) {
apply_loaded_prefab_bytes_with_path(editor_world, world, loaded, None);
}
fn apply_loaded_prefab_bytes_with_path(
editor_world: &mut EditorWorld,
world: &mut World,
loaded: &nightshade::filesystem::LoadedFile,
full_path: Option<&std::path::Path>,
) {
let prefab_scene = match load_scene_binary_from_bytes(&loaded.bytes) {
Ok(scene) => scene,
Err(error) => {
tracing::error!("Failed to load prefab '{}': {}", loaded.name, error);
return;
}
};
let source_uuid = prefab_scene.header.prefab_id;
let display_name_source = loaded.name.clone();
let source_path = full_path
.map(|path| path.to_string_lossy().to_string())
.unwrap_or_else(|| loaded.name.clone());
let prefab_display_name = if prefab_scene.header.name.is_empty() {
derive_scene_name_from_filename(&display_name_source)
} else {
prefab_scene.header.name.clone()
};
match nightshade::ecs::scene::commands::spawn_prefab_scene(world, &prefab_scene, None) {
Ok(result) => {
for warning in &result.warnings {
tracing::warn!("prefab spawn: {warning}");
}
for &root_entity in &result.root_entities {
world
.core
.add_components(root_entity, nightshade::ecs::world::PREFAB_SOURCE);
world.core.set_prefab_source(
root_entity,
nightshade::ecs::prefab::components::PrefabSource {
prefab_name: prefab_display_name.clone(),
source_path: Some(source_path.clone()),
source_uuid,
},
);
}
for (&entity, source_entity_uuid) in &result.entity_to_source_uuid {
editor_world
.resources
.prefab_instance_links
.entity_to_source
.insert(entity, *source_entity_uuid);
}
if let Some(uuid) = source_uuid {
editor_world
.resources
.prefab_instance_links
.imported_scenes
.insert(uuid, prefab_scene.clone());
}
for &root_entity in &result.root_entities {
crate::scene_writeback::register_subtree(
&mut editor_world.resources.project,
&mut editor_world.resources.editor_scene,
world,
root_entity,
);
}
editor_world.resources.loading.pending_fit_roots = result.root_entities.clone();
editor_world
.resources
.loading
.model_entities
.extend(result.root_entities);
editor_world.resources.loading.pending_fit_frames = 2;
editor_world.resources.project.mark_modified();
tracing::info!("Imported prefab '{}'", loaded.name);
}
Err(error) => {
tracing::error!("Failed to spawn prefab: {error}");
}
}
}
fn apply_loaded_map_bytes(
editor_world: &mut EditorWorld,
world: &mut World,
loaded: &nightshade::filesystem::LoadedFile,
) {
let is_json_extension = std::path::Path::new(&loaded.name)
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| extension.eq_ignore_ascii_case("json"));
let scene = if is_json_extension {
match nightshade::prelude::serde_json::from_slice::<Scene>(&loaded.bytes) {
Ok(mut scene) => {
scene.rebuild_uuid_index();
scene
}
Err(error) => {
tracing::error!("Failed to load '{}' as scene: {}", loaded.name, error);
return;
}
}
} else {
match load_scene_binary_from_bytes(&loaded.bytes) {
Ok(scene) => scene,
Err(error) => {
tracing::error!("Failed to load '{}' as binary map: {}", loaded.name, error);
return;
}
}
};
despawn_known_world_entities(editor_world, world);
editor_world.resources.editor_scene.clear();
editor_world.resources.loading.last_dropped_error = None;
let scene_name = if scene.header.name.is_empty() {
derive_scene_name_from_filename(&loaded.name)
} else {
scene.header.name.clone()
};
editor_world.resources.project = ProjectState {
scene: Scene::default(),
scene_name,
#[cfg(not(target_arch = "wasm32"))]
current_path: std::path::PathBuf::from(&loaded.name).canonicalize().ok(),
modified: false,
};
editor_world.resources.writeback_state = Default::default();
editor_world.resources.undo.clear();
apply_scene_to_world(editor_world, world, &scene);
editor_world.resources.project.scene = scene;
if crate::systems::sun::scene_has_lights(editor_world, world) {
crate::systems::sun::despawn_default(editor_world, world);
crate::systems::sun::adopt_scene_sun(editor_world, world);
} else if editor_world.resources.sun.sun_entity.is_none() {
crate::systems::sun::spawn_with_shadows(editor_world, world);
}
auto_refresh_prefab_instances(editor_world, world);
tracing::info!("Loaded map '{}'", loaded.name);
}
fn auto_refresh_prefab_instances(editor_world: &mut EditorWorld, world: &mut World) {
use nightshade::ecs::world::PREFAB_SOURCE;
let candidates: Vec<Entity> = world.core.query_entities(PREFAB_SOURCE).collect();
let mut refreshed = 0usize;
for entity in candidates {
let Some(source) = world.core.get_prefab_source(entity).cloned() else {
continue;
};
if source.source_path.is_none() && source.source_uuid.is_none() {
continue;
}
if !crate::systems::retained_ui::prefab_browser::source_reachable(editor_world, &source) {
continue;
}
crate::systems::retained_ui::prefab_browser::refresh_selected_instance(
editor_world,
world,
entity,
);
refreshed += 1;
}
if refreshed > 0 {
tracing::info!("Auto-refreshed {refreshed} prefab instance(s) from source");
}
}
fn apply_scene_to_world(editor_world: &mut EditorWorld, world: &mut World, scene: &Scene) {
world.resources.graphics.atmosphere = scene.atmosphere;
match &scene.hdr_skybox {
Some(SceneHdrSkybox::Reference { path }) => {
nightshade::ecs::world::commands::queue_render_command(
world,
nightshade::ecs::world::commands::RenderCommand::LoadHdrSkyboxFromPath {
path: std::path::PathBuf::from(path),
},
);
}
Some(SceneHdrSkybox::Embedded { data }) => {
nightshade::ecs::world::commands::queue_render_command(
world,
nightshade::ecs::world::commands::RenderCommand::LoadHdrSkybox {
hdr_data: data.clone(),
},
);
}
Some(SceneHdrSkybox::Asset { uuid }) => {
tracing::warn!("scene references HDR by uuid {uuid} but no asset registry is wired");
}
None => {}
}
match spawn_scene(world, scene, None) {
Ok(result) => {
for warning in &result.warnings {
tracing::warn!("scene spawn: {warning}");
}
let uuid_to_entity = result.uuid_to_entity.clone();
editor_world
.resources
.editor_scene
.replace_entities(result.uuid_to_entity);
editor_world
.resources
.prefab_instance_links
.entity_to_source
.clear();
editor_world
.resources
.prefab_instance_links
.entity_to_source
.extend(result.entity_to_source_uuid);
for scene_entity in &scene.entities {
let Some(runtime_entity) = uuid_to_entity.get(&scene_entity.uuid).copied() else {
continue;
};
editor_world
.resources
.editor_scene
.set_entity_layer(runtime_entity, scene_entity.layer);
editor_world
.resources
.editor_scene
.set_entity_chunk(runtime_entity, scene_entity.chunk_id);
}
editor_world.resources.loading.pending_fit_roots = result.root_entities.clone();
editor_world
.resources
.loading
.model_entities
.extend(result.root_entities);
editor_world.resources.loading.pending_fit_frames = 2;
}
Err(error) => {
tracing::error!("Failed to spawn map: {error}");
}
}
}
fn despawn_known_world_entities(editor_world: &mut EditorWorld, world: &mut World) {
editor_world.resources.loading.pending_imports.clear();
loading::clear_scene(editor_world, world);
}
fn derive_scene_name_from_filename(filename: &str) -> String {
std::path::Path::new(filename)
.file_stem()
.and_then(|stem| stem.to_str())
.map(|stem| stem.to_string())
.unwrap_or_else(|| "Imported".to_string())
}
fn flush_writeback(editor_world: &mut EditorWorld, world: &mut World) {
crate::scene_writeback::reconcile(
&mut editor_world.resources.project,
&mut editor_world.resources.editor_scene,
&mut editor_world.resources.writeback_state,
world,
true,
);
sync_settings(&mut editor_world.resources.project, world);
}