nightshade 0.57.0

A cross-platform data-oriented game engine.
Documentation
//! Whole-world save and load on nightshade_ecs snapshots.
//!
//! [`snapshot_world`] captures the shared entity allocator and the core
//! member world; [`restore_world`] rebuilds them in place. The retained-UI
//! and renderer member worlds are derived runtime state, so a restore
//! recreates them empty and the next frame's UI rebuild and renderer delta
//! sync repopulate them. Asset registries (meshes, textures, materials) are
//! not captured: the restored components reference assets by name, so the
//! same assets must already be registered when restoring. Restored rows read
//! as changed, so the renderer's delta sync picks the whole world up on the
//! next frame, and physics bodies respawn through the regular initialize pass
//! because component handles restore as unset. A capability plugin that owns
//! its own member world (physics, audio, navmesh) registers a
//! [`SnapshotCapability`](hooks::SnapshotCapability) when it is composed, and
//! its world is captured and restored alongside the app worlds so its rows
//! survive the round trip.

pub mod hooks;

use std::collections::BTreeMap;

use crate::ecs::bootstrap::{build_engine_ecs, register_core_components};
use crate::ecs::world::{CORE, RENDER, World};
use nightshade_ecs::EntityAllocator;
use nightshade_ecs::dynamic::{ComponentRegistry, DynWorld, DynWorldSnapshot, SnapshotError};

/// A serializable image of the engine world: the shared entity allocator,
/// the core member world, every app member world past the engine's derived
/// members, and one entry per composed capability that owns a member world,
/// keyed by the capability's snapshot key. App member worlds must register
/// with serde codecs (`dynamic_schema! { serde pub fn ... }`) to be captured.
#[derive(serde::Serialize, serde::Deserialize)]
pub struct WorldSnapshot {
    pub allocator: EntityAllocator,
    pub core: DynWorldSnapshot,
    pub game_worlds: Vec<DynWorldSnapshot>,
    pub capability_worlds: BTreeMap<String, DynWorldSnapshot>,
}

fn clone_allocator(allocator: &EntityAllocator) -> EntityAllocator {
    EntityAllocator {
        next_id: allocator.next_id,
        free_ids: allocator.free_ids.clone(),
        slots: allocator.slots.clone(),
    }
}

/// Captures the engine world into a [`WorldSnapshot`]. The core and every app
/// member world past the derived renderer slot are captured, and each composed
/// capability's member world is pulled out under its own key so a restore can
/// rebuild it with the capability's schema rather than an app registry.
pub fn snapshot_world(world: &World) -> Result<WorldSnapshot, SnapshotError> {
    let owners: Vec<(usize, &'static str)> = hooks::snapshot_capabilities(world)
        .iter()
        .filter_map(|capability| {
            (capability.member_world)(world).map(|index| (index, capability.key))
        })
        .collect();

    let mut game_worlds = Vec::new();
    let mut capability_worlds = BTreeMap::new();
    for (index, member) in world.ecs.worlds.iter().enumerate().skip(RENDER + 1) {
        match owners.iter().find(|(owned, _)| *owned == index) {
            Some((_, key)) => {
                capability_worlds.insert((*key).to_string(), member.snapshot()?);
            }
            None => game_worlds.push(member.snapshot()?),
        }
    }
    Ok(WorldSnapshot {
        allocator: clone_allocator(&world.ecs.allocator),
        core: world.ecs.worlds[CORE].snapshot()?,
        game_worlds,
        capability_worlds,
    })
}

/// Restores the engine world from a [`WorldSnapshot`] in place.
///
/// `game_registries` supplies one [`ComponentRegistry`] per app member
/// world in the snapshot, in registration order, typically the app's own
/// `register_<app>_components()`. The core and app member worlds and the
/// shared allocator come back exactly as captured; the retained-UI member
/// world comes back empty, so rebuild the app's UI after this returns.
/// Engine resource state that mirrors ECS content is reconciled: the
/// hierarchy index resyncs from scratch, the entity registry is rebuilt
/// from the restored name and guid components, the renderer performs a
/// full rebuild, queued commands are dropped, and the physics simulation
/// restarts from the restored components. The ECS resource map carries
/// across the swap, so plugin-owned resources keep their state exactly as
/// they did when they were plain resource fields.
pub fn restore_world(
    world: &mut World,
    snapshot: &WorldSnapshot,
    game_registries: Vec<ComponentRegistry>,
) -> Result<(), SnapshotError> {
    if game_registries.len() != snapshot.game_worlds.len() {
        return Err(SnapshotError::SchemaMismatch {
            expected: format!("{} app member worlds", snapshot.game_worlds.len()),
            found: format!("{} registries", game_registries.len()),
        });
    }

    let mut core = DynWorld::from_snapshot(register_core_components(), &snapshot.core)?;
    core.insert_missing_rows = true;
    core.set_change_detection(true);
    core.structural_logging = true;

    let mut ecs = build_engine_ecs();
    ecs.allocator = clone_allocator(&snapshot.allocator);
    ecs.worlds[CORE] = core;
    for (registry, member_snapshot) in game_registries.into_iter().zip(&snapshot.game_worlds) {
        let mut member = DynWorld::from_snapshot(registry, member_snapshot)?;
        member.insert_missing_rows = true;
        member.set_change_detection(true);
        member.structural_logging = true;
        ecs.worlds.push(member);
    }

    let capabilities = hooks::snapshot_capabilities(world);
    let mut restored = Vec::with_capacity(capabilities.len());
    for capability in capabilities {
        let index = restore_capability_world(
            &mut ecs,
            snapshot.capability_worlds.get(capability.key),
            capability.registry,
        )?;
        restored.push((capability.restore, index));
    }

    std::mem::swap(&mut ecs.resources, &mut world.ecs.resources);
    world.ecs = ecs;

    *world.res_mut::<nightshade_ecs::dynamic::HierarchyIndex>() =
        nightshade_ecs::dynamic::HierarchyIndex::default();
    world
        .res_mut::<crate::ecs::world::commands::CommandQueues>()
        .ecs
        .clear();
    world
        .res_mut::<crate::ecs::world::commands::CommandQueues>()
        .render
        .clear();

    rebuild_entity_registry(world);
    reset_retained_ui(world);
    reconcile_texture_ledger(world);
    world
        .res_mut::<crate::render::mesh_state::MeshRenderState>()
        .request_full_rebuild();

    for (restore, index) in restored {
        restore(world, index);
    }

    Ok(())
}

/// Rebuilds a capability plugin's member world from its snapshot and appends it
/// after the app worlds, returning its index so the plugin's resource can be
/// repointed at it. `registry` reconstructs the world's engine-owned schema (an
/// app never sees these worlds). Returns `None` when the snapshot carried no
/// such world, which is the case whenever nothing wrote one of the plugin's
/// components before the capture.
fn restore_capability_world(
    ecs: &mut nightshade_ecs::dynamic::DynEcs,
    snapshot: Option<&DynWorldSnapshot>,
    registry: fn() -> ComponentRegistry,
) -> Result<Option<usize>, SnapshotError> {
    let Some(snapshot) = snapshot else {
        return Ok(None);
    };
    let mut member = DynWorld::from_snapshot(registry(), snapshot)?;
    member.insert_missing_rows = true;
    member.set_change_detection(true);
    member.structural_logging = true;
    let index = ecs.worlds.len();
    ecs.worlds.push(member);
    Ok(Some(index))
}

fn rebuild_entity_registry(world: &mut World) {
    let mut names = std::collections::HashMap::new();
    for (entity, name) in world.ecs.worlds[CORE]
        .query_ref::<&crate::ecs::primitives::Name>()
        .iter()
    {
        names.insert(name.0.clone(), entity);
    }
    let mut guid_index = std::collections::HashMap::new();
    let mut highest_guid = 0u64;
    for (entity, guid) in world.ecs.worlds[CORE]
        .query_ref::<&crate::ecs::primitives::Guid>()
        .iter()
    {
        guid_index.insert(guid.0, entity);
        highest_guid = highest_guid.max(guid.0);
    }
    let registry = &mut world.res_mut::<crate::ecs::entity_registry::EntityRegistry>();
    registry.names = names;
    registry.guid_index = guid_index;
    registry.tags.clear();
    registry.next_guid = registry.next_guid.max(highest_guid.saturating_add(1));
}

/// Releases texture references whose owning entity did not survive the
/// restore. Entities spawned after the capture died with the swap without
/// their despawn-side release running, so their ledger entries would
/// otherwise hold reference counts forever.
fn reconcile_texture_ledger(world: &mut World) {
    let mut texture_cache =
        std::mem::take(world.res_mut::<crate::render::wgpu::texture_cache::TextureCache>());
    crate::render::wgpu::texture_cache::texture_cache_release_dead_entities(
        &mut texture_cache,
        |entity| world.is_alive(entity),
    );
    *world.res_mut::<crate::render::wgpu::texture_cache::TextureCache>() = texture_cache;
}

fn reset_retained_ui(world: &mut World) {
    let dirty = world.res_mut::<crate::ui::resources::RetainedUiDirty>();
    dirty.layout_dirty = true;
    dirty.render_dirty = true;
    let interaction = world.res_mut::<crate::ui::resources::RetainedUiInteraction>();
    interaction.focused_entity = None;
    interaction.hovered_entity = None;
    interaction.active_entity = None;
    interaction.last_click = None;
}

/// Serializes a [`WorldSnapshot`] to bytes.
#[cfg(feature = "assets")]
pub fn save_world_snapshot_to_bytes(world: &World) -> Result<Vec<u8>, SnapshotError> {
    let snapshot = snapshot_world(world)?;
    bincode::serde::encode_to_vec(&snapshot, bincode::config::legacy())
        .map_err(|error| SnapshotError::Codec(error.to_string()))
}

/// Restores the engine world from bytes produced by
/// [`save_world_snapshot_to_bytes`].
#[cfg(feature = "assets")]
pub fn load_world_snapshot_from_bytes(
    world: &mut World,
    bytes: &[u8],
    game_registries: Vec<ComponentRegistry>,
) -> Result<(), SnapshotError> {
    let (snapshot, _): (WorldSnapshot, usize) =
        bincode::serde::decode_from_slice(bytes, bincode::config::legacy())
            .map_err(|error| SnapshotError::Codec(error.to_string()))?;
    restore_world(world, &snapshot, game_registries)
}