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