nightshade-api 0.41.0

Procedural high level API for the nightshade game engine
Documentation
//! The portable entry points and the shared engine state behind both loop modes.

use nightshade::prelude::*;

pub(crate) const RESERVED_PREFIX: &str = "api::";
pub(crate) const CAMERA_NAME_PREFIX: &str = "api::camera::";
pub(crate) const CAMERA_ORBIT: &str = "api::camera::orbit";
pub(crate) const CAMERA_FLY: &str = "api::camera::fly";
#[cfg(feature = "physics")]
pub(crate) const CAMERA_FIRST_PERSON: &str = "api::camera::first_person";
pub(crate) const CAMERA_FIXED: &str = "api::camera::fixed";
#[cfg(feature = "physics")]
pub(crate) const PLAYER_NAME: &str = "api::player";
pub(crate) const SUN_NAME: &str = "api::sun";
pub(crate) const DRAW_MATERIAL: &str = "api::draw";
pub(crate) const DRAW_CUBE_POOL: &str = "api::draw::cube";
pub(crate) const DRAW_SPHERE_POOL: &str = "api::draw::sphere";
pub(crate) const DRAW_LINES_POOL: &str = "api::draw::lines";
pub(crate) const MATERIAL_PREFIX: &str = "api::material::";

type SetupFunction<Data> = Box<dyn FnOnce(&mut World) -> Data>;
type UpdateFunction<Data> = Box<dyn FnMut(&mut World, &mut Data)>;

pub(crate) fn register_named(world: &mut World, name: &str, entity: Entity) {
    world
        .resources
        .entities
        .names
        .insert(name.to_string(), entity);
}

pub(crate) fn lookup_named(world: &mut World, name: &str) -> Option<Entity> {
    let cached = world
        .resources
        .entities
        .names
        .get(name)
        .copied()
        .filter(|&entity| world.core.get_name(entity).is_some());
    if cached.is_some() {
        return cached;
    }
    let found = nightshade::ecs::world::commands::find_entity_by_name(world, name)?;
    register_named(world, name, found);
    Some(found)
}

pub(crate) struct ApiState<Data> {
    pub(crate) setup: Option<SetupFunction<Data>>,
    pub(crate) update: Option<UpdateFunction<Data>>,
    pub(crate) data: Option<Data>,
    pub(crate) clears_draw_pools: bool,
    pub(crate) frame_limit: Option<u32>,
    pub(crate) frames_rendered: u32,
}

pub(crate) fn frame_limit_from_environment() -> Option<u32> {
    std::env::var("NIGHTSHADE_API_FRAMES")
        .ok()
        .and_then(|value| value.parse().ok())
}

impl<Data: 'static> State for ApiState<Data> {
    fn initialize(&mut self, world: &mut World) {
        apply_defaults(world);
        if let Some(setup) = self.setup.take() {
            self.data = Some(setup(world));
        }
    }

    fn run_systems(&mut self, world: &mut World) {
        if let Some(limit) = self.frame_limit {
            self.frames_rendered += 1;
            if self.frames_rendered >= limit {
                world.resources.window.should_exit = true;
            }
        }
        escape_key_exit_system(world);
        run_camera_systems(world);
        if self.clears_draw_pools {
            crate::draw::clear_draw_pools(world);
        }
        if let (Some(update), Some(data)) = (self.update.as_mut(), self.data.as_mut()) {
            update(world, data);
        }
    }
}

fn apply_defaults(world: &mut World) {
    world.resources.render_settings.atmosphere = Atmosphere::Sky;
    world.resources.debug_draw.show_grid = true;
    #[cfg(feature = "physics")]
    {
        world.resources.physics.enabled = true;
    }
    let sun = spawn_sun(world);
    world.core.set_name(sun, Name(SUN_NAME.to_string()));
    register_named(world, SUN_NAME, sun);
    load_procedural_textures(world);
    crate::draw::initialize_draw_pools(world);
    crate::camera::orbit_camera(world, Vec3::zeros(), 8.0);
}

fn run_camera_systems(world: &mut World) {
    let Some(camera) = world.resources.active_camera else {
        return;
    };
    let drives_controllers = world
        .core
        .get_name(camera)
        .is_some_and(|name| name.0 == CAMERA_ORBIT || name.0 == CAMERA_FLY);
    if drives_controllers {
        camera_controllers_system(world);
    }
    #[cfg(feature = "physics")]
    {
        let drives_character = world
            .core
            .get_name(camera)
            .is_some_and(|name| name.0 == CAMERA_FIRST_PERSON);
        if drives_character {
            first_person_camera_look_system(world);
        }
    }
}

/// Runs a program from two closures, handing the engine the main loop.
///
/// `setup` runs once after the renderer is ready and returns your state,
/// anything from a single [`Entity`] to a struct of your own. `update` runs
/// every frame and receives that state back mutably. This is the portable
/// form: it is the only entry point on wasm, where the browser owns the loop.
/// On native, prefer [`open`](crate::prelude::open) and
/// [`frame`](crate::prelude::frame) for straight-line code.
///
/// ```ignore
/// run(
///     |world| spawn_cube(world, vec3(0.0, 0.5, 0.0)),
///     |world, cube| {
///         let step = delta_time(world);
///         rotate(world, *cube, Vec3::y(), step);
///     },
/// )
/// .unwrap();
/// ```
pub fn run<Data: 'static>(
    setup: impl FnOnce(&mut World) -> Data + 'static,
    update: impl FnMut(&mut World, &mut Data) + 'static,
) -> Result<(), Box<dyn std::error::Error>> {
    launch(ApiState {
        setup: Some(Box::new(setup)),
        update: Some(Box::new(update)),
        data: None,
        clears_draw_pools: true,
        frame_limit: frame_limit_from_environment(),
        frames_rendered: 0,
    })
}

/// Runs a static scene: `setup` once, then the engine just renders it.
///
/// The default orbit camera stays interactive, so this is the shortest path
/// to a scene you can look around in.
pub fn run_scene(
    setup: impl FnOnce(&mut World) + 'static,
) -> Result<(), Box<dyn std::error::Error>> {
    run(setup, |_, _| {})
}