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