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