nightshade-editor 0.14.2

Interactive map editor for the Nightshade game engine
use crate::editor_scene::EditorScene;
use crate::project::ProjectState;
use nightshade::ecs::scene::{
    AssetUuid, SceneEntity, SceneHdrSkybox, capture_scene_settings,
    entity_to_scene_entity_with_uuids,
};
use nightshade::ecs::world::LOCAL_TRANSFORM;
use nightshade::prelude::{Entity, World};
use std::collections::HashSet;

#[derive(Default)]
pub struct WritebackState {
    /// Last world entity count we observed in `reconcile`. Used as a
    /// cheap "did anything change?" signal so we only walk the world
    /// when the editor likely added or removed entities.
    pub last_world_entity_count: usize,
    /// Set after switching scenes or applying undo/redo so the next
    /// reconcile pass forces a full re-sync.
    pub needs_full_resync: bool,
}

impl WritebackState {
    pub fn mark_full_resync(&mut self) {
        self.needs_full_resync = true;
    }
}

pub fn sync_entity(
    project: &mut ProjectState,
    editor_scene: &EditorScene,
    world: &World,
    entity: Entity,
) {
    let Some(uuid) = editor_scene.uuid_for(entity) else {
        return;
    };
    let parent_uuid = parent_uuid_for(world, editor_scene, entity);
    let mut scene_entity = entity_to_scene_entity_with_uuids(
        world,
        entity,
        uuid,
        parent_uuid,
        &editor_scene.entity_to_uuid,
    );
    scene_entity.layer = editor_scene.entity_layer(entity);
    scene_entity.chunk_id = editor_scene.entity_chunk(entity);
    write_scene_entity(project, scene_entity);
}

pub fn sync_all_known_entities(
    project: &mut ProjectState,
    editor_scene: &EditorScene,
    world: &World,
) {
    let entities: Vec<Entity> = editor_scene.known_entities().collect();
    for entity in entities {
        sync_entity(project, editor_scene, world, entity);
    }
}

pub fn add_entity(
    project: &mut ProjectState,
    editor_scene: &mut EditorScene,
    world: &World,
    entity: Entity,
) {
    if editor_scene.is_scaffolding(entity) {
        return;
    }
    if editor_scene.uuid_for(entity).is_some() {
        sync_entity(project, editor_scene, world, entity);
        return;
    }
    let parent_uuid = parent_uuid_for(world, editor_scene, entity);
    let uuid = AssetUuid::random();
    editor_scene.insert(uuid, entity);
    let mut scene_entity = entity_to_scene_entity_with_uuids(
        world,
        entity,
        uuid,
        parent_uuid,
        &editor_scene.entity_to_uuid,
    );
    scene_entity.layer = editor_scene.entity_layer(entity);
    scene_entity.chunk_id = editor_scene.entity_chunk(entity);
    write_scene_entity(project, scene_entity);
}

/// Walks a freshly-spawned tree (root and descendants) and registers
/// every entity that isn't scaffolding into the active Scene. Used by
/// the GLB import path, scene apply, AddEntityChild, and dev-tool spawns
/// so the Scene file reflects what the user just placed without
/// requiring per-spawn manual writeback calls.
///
/// Done in two passes so the entity-to-uuid map is complete before any
/// `SceneEntity` is written. The first pass mints uuids for every
/// non-scaffolding entity in the subtree; the second pass syncs each
/// entity into the active scene. Without this split, the root's
/// `SceneAnimationPlayer.bone_name_to_node` would serialize empty
/// (descendants don't have uuids yet at the time the root is written),
/// and the in-memory `project.scene` mirror would disagree with what
/// `world_to_scene_with_uuids` later produces on disk.
pub fn register_subtree(
    project: &mut ProjectState,
    editor_scene: &mut EditorScene,
    world: &World,
    root: Entity,
) {
    let mut entities = vec![root];
    entities.extend(nightshade::ecs::transform::queries::query_descendants(
        world, root,
    ));
    for entity in &entities {
        if editor_scene.is_scaffolding(*entity) {
            continue;
        }
        if editor_scene.uuid_for(*entity).is_none() {
            let uuid = AssetUuid::random();
            editor_scene.insert(uuid, *entity);
        }
    }
    for entity in &entities {
        if editor_scene.is_scaffolding(*entity) {
            continue;
        }
        sync_entity(project, editor_scene, world, *entity);
    }
}

pub fn remove_entity(project: &mut ProjectState, editor_scene: &mut EditorScene, entity: Entity) {
    let Some(uuid) = editor_scene.remove_entity(entity) else {
        return;
    };
    project.scene.remove_entity(uuid);
    project.mark_modified();
}

/// Walks the world after frames where entity count may have changed, and
/// brings the active Scene back into agreement with World. Only runs when
/// world entity count differs from last reconcile, or when `force` is true
/// (set after undo/redo or after switching scenes).
pub fn reconcile(
    project: &mut ProjectState,
    editor_scene: &mut EditorScene,
    writeback_state: &mut WritebackState,
    world: &mut World,
    force: bool,
) {
    if !force {
        let count = world_entity_count(world);
        if count == writeback_state.last_world_entity_count {
            return;
        }
        writeback_state.last_world_entity_count = count;
    }

    let mut world_entities: HashSet<Entity> = HashSet::new();
    world
        .core
        .query()
        .with(LOCAL_TRANSFORM)
        .iter(|entity, _, _| {
            if !editor_scene.is_scaffolding(entity) {
                world_entities.insert(entity);
            }
        });

    let known: HashSet<Entity> = editor_scene.known_entities().collect();

    for entity in &known {
        if !world_entities.contains(entity) {
            remove_entity(project, editor_scene, *entity);
        }
    }

    for entity in &world_entities {
        if !known.contains(entity) {
            add_entity(project, editor_scene, world, *entity);
        }
    }

    if force {
        sync_all_known_entities(project, editor_scene, world);
    }

    writeback_state.last_world_entity_count = world_entity_count(world);
}

pub fn sync_settings(project: &mut ProjectState, world: &World) {
    project.scene.atmosphere = world.resources.graphics.atmosphere;
    project.scene.settings = capture_scene_settings(world);
}

pub fn sync_hdr_skybox(project: &mut ProjectState, hdr: Option<SceneHdrSkybox>) {
    project.scene.hdr_skybox = hdr;
    project.mark_modified();
}

fn world_entity_count(world: &World) -> usize {
    world.core.get_all_entities().len()
}

fn parent_uuid_for(world: &World, scene: &EditorScene, entity: Entity) -> Option<AssetUuid> {
    world
        .core
        .get_parent(entity)
        .and_then(|parent| parent.0)
        .and_then(|parent_entity| scene.uuid_for(parent_entity))
}

fn write_scene_entity(project: &mut ProjectState, scene_entity: SceneEntity) {
    let scene = &mut project.scene;
    let uuid = scene_entity.uuid;
    if let Some(existing) = scene.find_entity_mut(uuid) {
        *existing = scene_entity;
    } else {
        scene.add_entity(scene_entity);
    }
    project.mark_modified();
}