nightshade 0.51.0

A cross-platform data-oriented game engine.
Documentation
//! Whole-world save and load on freecs snapshots.
//!
//! [`snapshot_world`] captures the shared entity allocator and the core
//! member world; [`restore_world`] rebuilds them in place. The retained-UI
//! member world is runtime state, so a restore recreates it empty and the
//! app rebuilds its UI afterwards. 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.

use crate::ecs::world::{CORE, UI, World, build_engine_ecs, register_core_components};
use freecs::EntityAllocator;
use freecs::dynamic::{ComponentRegistry, DynWorld, DynWorldSnapshot, SnapshotError};

/// A serializable image of the engine world: the shared entity allocator,
/// the core member world, and every app member world past the retained-UI
/// slot. 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>,
}

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`].
pub fn snapshot_world(world: &World) -> Result<WorldSnapshot, SnapshotError> {
    let mut game_worlds = Vec::new();
    for member in world.ecs.worlds.iter().skip(UI + 1) {
        game_worlds.push(member.snapshot()?);
    }
    Ok(WorldSnapshot {
        allocator: clone_allocator(&world.ecs.allocator),
        core: world.ecs.worlds[CORE].snapshot()?,
        game_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.
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;

    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;
        ecs.worlds.push(member);
    }
    world.ecs = ecs;

    world.resources.hierarchy = freecs::dynamic::HierarchyIndex::default();
    world.resources.commands.ecs.clear();
    world.resources.commands.render.clear();

    rebuild_entity_registry(world);
    reset_retained_ui(world);
    reconcile_texture_ledger(world);
    world.resources.mesh_render_state.request_full_rebuild();

    #[cfg(feature = "physics")]
    reset_physics(world);

    Ok(())
}

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.resources.entities;
    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 World { ecs, resources } = world;
    crate::render::wgpu::texture_cache::texture_cache_release_dead_entities(
        &mut resources.texture_cache,
        |entity| ecs.is_alive(crate::render_driver::scene_entity(entity)),
    );
}

fn reset_retained_ui(world: &mut World) {
    let retained_ui = &mut world.resources.retained_ui;
    retained_ui.dirty.layout_dirty = true;
    retained_ui.dirty.render_dirty = true;
    retained_ui.interaction.focused_entity = None;
    retained_ui.interaction.hovered_entity = None;
    retained_ui.interaction.active_entity = None;
    retained_ui.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::serialize(&snapshot).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 =
        bincode::deserialize(bytes).map_err(|error| SnapshotError::Codec(error.to_string()))?;
    restore_world(world, &snapshot, game_registries)
}

#[cfg(feature = "physics")]
fn reset_physics(world: &mut World) {
    let physics = &mut world.resources.physics;
    let gravity = physics.gravity;
    let fixed_timestep = physics.fixed_timestep;
    let max_substeps = physics.max_substeps;
    let enabled = physics.enabled;
    let debug_draw = physics.debug_draw;
    *physics = crate::ecs::physics::resources::PhysicsWorld::default();
    physics.gravity = gravity;
    physics.fixed_timestep = fixed_timestep;
    physics.max_substeps = max_substeps;
    physics.enabled = enabled;
    physics.debug_draw = debug_draw;
}