nightshade-api 0.43.0

Procedural high level API for the nightshade game engine
Documentation
//! The caller-owned main loop: [`open`] a window, then [`frame`] it yourself.

use crate::runner::ApiState;
use nightshade::prelude::*;
use nightshade::run::pump::{PumpShell, pump_frame, pump_shell_new, pump_shell_ready};

/// Window settings for [`open_with`]. Plain data with sensible defaults.
pub struct Window {
    pub title: String,
    pub size: Option<(u32, u32)>,
}

impl Default for Window {
    fn default() -> Self {
        Self {
            title: "nightshade".to_string(),
            size: None,
        }
    }
}

/// A running engine you drive one [`frame`] at a time.
///
/// `world` is the real engine world, yours to read and mutate between frames.
/// `frame_limit` stops the loop after that many frames, and is seeded from
/// the `NIGHTSHADE_API_FRAMES` environment variable so examples double as
/// smoke tests.
pub struct App {
    pub world: World,
    pub frame_limit: Option<u32>,
    frames_rendered: u32,
    shell: PumpShell,
}

/// Opens a window with the default settings and returns once the renderer is
/// ready and the standard scene defaults (sky, sun, grid, orbit camera) are in
/// place.
pub fn open() -> App {
    open_with(Window::default())
}

/// Opens a window with the given settings. See [`open`].
pub fn open_with(window: Window) -> App {
    let state = ApiState::<()> {
        setup: None,
        update: None,
        data: None,
        clears_draw_pools: false,
        frame_limit: None,
        frames_rendered: 0,
    };
    let mut shell =
        pump_shell_new(Box::new(state)).expect("failed to create the engine event loop");
    shell.context.world.resources.window.title = window.title;
    shell.context.world.resources.window.initial_size = window.size;

    let mut pumps_without_window = 0;
    while !pump_shell_ready(&shell) {
        if !pump_frame(&mut shell) {
            break;
        }
        let window_exists = shell.context.world.resources.window.handle.is_some();
        if window_exists && shell.context.initialized && shell.context.renderer.is_none() {
            panic!("failed to create the renderer, see the log for the error");
        }
        if !window_exists {
            pumps_without_window += 1;
            if pumps_without_window > 10000 {
                panic!("failed to create the window, see the log for the error");
            }
        }
    }

    let mut world = World::default();
    std::mem::swap(&mut world, &mut shell.context.world);

    schedule_remove(
        &mut world.resources.schedules.frame,
        system_names::RESET_MOUSE,
    );
    schedule_remove(
        &mut world.resources.schedules.frame,
        system_names::RESET_KEYBOARD,
    );
    schedule_remove(
        &mut world.resources.schedules.frame,
        system_names::RESET_TOUCH,
    );

    let frame_limit = crate::runner::frame_limit_from_environment();

    App {
        world,
        frame_limit,
        frames_rendered: 0,
        shell,
    }
}

/// Builds a scene and saves it as a png, with no visible window. The window
/// and renderer come up hidden, `setup` runs against the same defaults
/// [`open`] applies, the scene settles for half a second of frames so
/// streamed textures and lighting captures land, then the frame is captured
/// to `path`.
///
/// ```ignore
/// render_image(1920, 1080, "scene.png", |world| {
///     spawn_floor(world, 10.0);
///     let gem = spawn_torus(world, vec3(0.0, 1.5, 0.0));
///     set_emissive(world, gem, [0.3, 0.8, 1.0], 8.0);
/// });
/// ```
pub fn render_image(
    width: u32,
    height: u32,
    path: impl Into<std::path::PathBuf>,
    setup: impl FnOnce(&mut World),
) {
    let state = ApiState::<()> {
        setup: None,
        update: None,
        data: None,
        clears_draw_pools: false,
        frame_limit: None,
        frames_rendered: 0,
    };
    let mut shell =
        pump_shell_new(Box::new(state)).expect("failed to create the engine event loop");
    shell.context.world.resources.window.start_hidden = true;
    shell.context.world.resources.window.initial_size = Some((width, height));

    while !pump_shell_ready(&shell) {
        if !pump_frame(&mut shell) {
            return;
        }
    }

    setup(&mut shell.context.world);

    for _ in 0..30 {
        tick_hidden(&mut shell);
    }
    crate::environment::screenshot(&mut shell.context.world, path.into());
    for _ in 0..10 {
        tick_hidden(&mut shell);
    }
}

fn tick_hidden(shell: &mut PumpShell) {
    let context = &mut shell.context;
    let Some(renderer) = context.renderer.as_mut() else {
        return;
    };
    if let Some(next_state) = tick_offscreen(&mut context.world, context.state.as_mut(), renderer) {
        context.state = next_state;
    }
}

/// Pumps events and renders one frame. Returns false when the window closes,
/// escape exits, or the frame limit is reached.
///
/// Everything drawn with the `draw_` functions since the previous call is
/// visible for this frame and cleared afterward. Edge triggered input
/// ([`key_pressed`](crate::prelude::key_pressed),
/// [`mouse_clicked`](crate::prelude::mouse_clicked), the mouse deltas)
/// reflects this frame's events and stays readable until the next call. The
/// per frame input reset that normally runs inside the engine's frame
/// schedule runs here instead, before pumping, so those flags survive the
/// gap where your code runs.
pub fn frame(app: &mut App) -> bool {
    if let Some(limit) = app.frame_limit
        && app.frames_rendered >= limit
    {
        return false;
    }

    nightshade::ecs::input::systems::reset_mouse_system(&mut app.world);
    nightshade::ecs::input::systems::reset_keyboard_system(&mut app.world);
    nightshade::ecs::input::systems::reset_touch_system(&mut app.world);

    std::mem::swap(&mut app.world, &mut app.shell.context.world);
    let alive = pump_frame(&mut app.shell);
    std::mem::swap(&mut app.world, &mut app.shell.context.world);

    crate::draw::clear_draw_pools(&mut app.world);
    app.frames_rendered += 1;

    alive
}