Skip to main content

nightshade_api/
runner.rs

1//! The portable entry points and the shared engine state behind both loop modes.
2
3use nightshade::prelude::*;
4
5#[cfg(any(feature = "picking", feature = "navmesh"))]
6pub(crate) const RESERVED_PREFIX: &str = "api::";
7pub(crate) const CAMERA_NAME_PREFIX: &str = "api::camera::";
8pub(crate) const CAMERA_ORBIT: &str = "api::camera::orbit";
9pub(crate) const CAMERA_FLY: &str = "api::camera::fly";
10#[cfg(feature = "physics")]
11pub(crate) const CAMERA_FIRST_PERSON: &str = "api::camera::first_person";
12pub(crate) const CAMERA_FIXED: &str = "api::camera::fixed";
13#[cfg(feature = "physics")]
14pub(crate) const PLAYER_NAME: &str = "api::player";
15pub(crate) const SUN_NAME: &str = "api::sun";
16pub(crate) const DRAW_MATERIAL: &str = "api::draw";
17pub(crate) const DRAW_CUBE_POOL: &str = "api::draw::cube";
18pub(crate) const DRAW_SPHERE_POOL: &str = "api::draw::sphere";
19pub(crate) const DRAW_CYLINDER_POOL: &str = "api::draw::cylinder";
20pub(crate) const DRAW_CONE_POOL: &str = "api::draw::cone";
21pub(crate) const DRAW_TORUS_POOL: &str = "api::draw::torus";
22pub(crate) const DRAW_LINES_POOL: &str = "api::draw::lines";
23pub(crate) const MATERIAL_PREFIX: &str = "api::material::";
24pub(crate) const UI_ROOT_NAME: &str = "api::ui::root";
25
26type SetupFunction<Data> = Box<dyn FnOnce(&mut World) -> Data>;
27type UpdateFunction<Data> = Box<dyn FnMut(&mut World, &mut Data)>;
28
29pub(crate) fn register_named(world: &mut World, name: &str, entity: Entity) {
30    world
31        .resources
32        .entities
33        .names
34        .insert(name.to_string(), entity);
35}
36
37pub(crate) fn lookup_named(world: &mut World, name: &str) -> Option<Entity> {
38    let cached = world
39        .resources
40        .entities
41        .names
42        .get(name)
43        .copied()
44        .filter(|&entity| world.core.get_name(entity).is_some());
45    if cached.is_some() {
46        return cached;
47    }
48    let found = nightshade::ecs::world::commands::find_entity_by_name(world, name)?;
49    register_named(world, name, found);
50    Some(found)
51}
52
53pub(crate) struct ApiState<Data> {
54    pub(crate) setup: Option<SetupFunction<Data>>,
55    pub(crate) update: Option<UpdateFunction<Data>>,
56    pub(crate) data: Option<Data>,
57    pub(crate) clears_draw_pools: bool,
58    pub(crate) frame_limit: Option<u32>,
59    pub(crate) frames_rendered: u32,
60}
61
62pub(crate) fn frame_limit_from_environment() -> Option<u32> {
63    std::env::var("NIGHTSHADE_API_FRAMES")
64        .ok()
65        .and_then(|value| value.parse().ok())
66}
67
68impl<Data: 'static> State for ApiState<Data> {
69    fn initialize(&mut self, world: &mut World) {
70        apply_defaults(world);
71        if let Some(setup) = self.setup.take() {
72            self.data = Some(setup(world));
73        }
74    }
75
76    fn run_systems(&mut self, world: &mut World) {
77        if let Some(limit) = self.frame_limit {
78            self.frames_rendered += 1;
79            if self.frames_rendered >= limit {
80                world.resources.window.should_exit = true;
81            }
82        }
83        escape_key_exit_system(world);
84        run_camera_systems(world);
85        if self.clears_draw_pools {
86            crate::draw::clear_draw_pools(world);
87        }
88        if let (Some(update), Some(data)) = (self.update.as_mut(), self.data.as_mut()) {
89            update(world, data);
90        }
91    }
92}
93
94fn apply_defaults(world: &mut World) {
95    world.resources.render_settings.atmosphere = Atmosphere::Sky;
96    world.resources.debug_draw.show_grid = false;
97    world.resources.user_interface.enabled = true;
98    world.resources.retained_ui.enabled = true;
99    #[cfg(feature = "physics")]
100    {
101        world.resources.physics.enabled = true;
102    }
103    let sun = spawn_sun(world);
104    world.core.set_name(sun, Name(SUN_NAME.to_string()));
105    register_named(world, SUN_NAME, sun);
106    load_procedural_textures(world);
107    crate::draw::initialize_draw_pools(world);
108    crate::camera::orbit_camera(world, Vec3::zeros(), 8.0);
109}
110
111fn run_camera_systems(world: &mut World) {
112    let Some(camera) = world.resources.active_camera else {
113        return;
114    };
115    let drives_controllers = world
116        .core
117        .get_name(camera)
118        .is_some_and(|name| name.0 == CAMERA_ORBIT || name.0 == CAMERA_FLY);
119    if drives_controllers {
120        camera_controllers_system(world);
121    }
122    #[cfg(feature = "physics")]
123    {
124        let drives_character = world
125            .core
126            .get_name(camera)
127            .is_some_and(|name| name.0 == CAMERA_FIRST_PERSON);
128        if drives_character {
129            first_person_camera_look_system(world);
130        }
131    }
132}
133
134/// Runs a program from two closures, handing the engine the main loop.
135///
136/// `setup` runs once after the renderer is ready and returns your state,
137/// anything from a single [`Entity`] to a struct of your own. `update` runs
138/// every frame and receives that state back mutably. This is the portable
139/// form: it is the only entry point on wasm, where the browser owns the loop.
140/// On native, prefer [`open`](crate::prelude::open) and
141/// [`frame`](crate::prelude::frame) for straight-line code.
142///
143/// ```ignore
144/// run(
145///     |world| spawn_cube(world, vec3(0.0, 0.5, 0.0)),
146///     |world, cube| {
147///         let step = delta_time(world);
148///         rotate(world, *cube, Vec3::y(), step);
149///     },
150/// )
151/// .unwrap();
152/// ```
153pub fn run<Data: 'static>(
154    setup: impl FnOnce(&mut World) -> Data + 'static,
155    update: impl FnMut(&mut World, &mut Data) + 'static,
156) -> Result<(), Box<dyn std::error::Error>> {
157    launch(ApiState {
158        setup: Some(Box::new(setup)),
159        update: Some(Box::new(update)),
160        data: None,
161        clears_draw_pools: true,
162        frame_limit: frame_limit_from_environment(),
163        frames_rendered: 0,
164    })
165}
166
167/// Runs a static scene: `setup` once, then the engine just renders it.
168///
169/// The default orbit camera stays interactive, so this is the shortest path
170/// to a scene you can look around in.
171pub fn run_scene(
172    setup: impl FnOnce(&mut World) + 'static,
173) -> Result<(), Box<dyn std::error::Error>> {
174    run(setup, |_, _| {})
175}
176
177/// Folds an array of per-frame update functions into a single update that runs
178/// them in order. This is what [`run!`] expands to; call it directly as
179/// `run(setup, systems([a, b, c]))` if you prefer a plain function. Each entry
180/// is a `fn(&mut World, &mut Data)`, so they are named systems or non-capturing
181/// closures, all sharing the state `setup` returned.
182pub fn systems<Data, const N: usize>(
183    updates: [fn(&mut World, &mut Data); N],
184) -> impl FnMut(&mut World, &mut Data) {
185    move |world, data| {
186        for &update in &updates {
187            update(world, data);
188        }
189    }
190}
191
192/// Runs a program from a setup expression and one or more per-frame update
193/// expressions, the variadic form of [`run`]. Setup runs once and returns your
194/// state; each update runs every frame, in the order given, and receives that
195/// state. Returns the same `Result` as [`run`], so `main` can return it.
196///
197/// ```ignore
198/// fn main() -> Result<(), Box<dyn std::error::Error>> {
199///     run!(
200///         |world| spawn_cube(world, vec3(0.0, 0.5, 0.0)),
201///         |world, cube| rotate(world, *cube, Vec3::y(), delta_time(world)),
202///     )
203/// }
204/// ```
205///
206/// Each update is a `fn(&mut World, &mut Data)` or a non-capturing closure of
207/// that shape, where `Data` is whatever setup returns. They all see the same
208/// state, so a program splits cleanly into named systems:
209///
210/// ```ignore
211/// run!(setup, handle_input, move_player, check_collisions)
212/// ```
213///
214/// For a static scene with no per-frame work, use [`run_scene`] instead.
215#[macro_export]
216macro_rules! run {
217    ($setup:expr, $($update:expr),+ $(,)?) => {
218        $crate::prelude::run($setup, $crate::prelude::systems([$($update),+]))
219    };
220}