nightshade-api 0.38.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;

    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");
            }
        }
    }

    if let Some((width, height)) = window.size
        && let Some(handle) = shell.context.world.resources.window.handle.clone()
        && let Some(size) = handle.request_inner_size(winit::dpi::PhysicalSize::new(width, height))
        && size.width > 0
        && size.height > 0
    {
        shell.context.world.resources.window.cached_viewport_size = Some((size.width, size.height));
        if let Some(renderer) = shell.context.renderer.as_mut()
            && let Err(error) = renderer.resize_surface(size.width, size.height)
        {
            tracing::error!("Failed to resize surface: {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,
    }
}

/// 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
}