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        #[cfg(all(feature = "audio", target_arch = "wasm32"))]
88        nightshade::plugins::audio::systems::lazy_initialize_audio_system(world);
89        #[cfg(feature = "gamepad")]
90        {
91            nightshade::plugins::gamepad::gamepad_input_system(world);
92            let gamepad_events = world.resources.input.gamepad.events.clone();
93            for event in gamepad_events {
94                world
95                    .resources
96                    .input
97                    .events
98                    .push(nightshade::ecs::input::events::AppEvent::Gamepad(event));
99            }
100        }
101        escape_key_exit_system(world);
102        run_camera_systems(world);
103        if self.clears_draw_pools {
104            crate::draw::clear_draw_pools(world);
105        }
106        if let (Some(update), Some(data)) = (self.update.as_mut(), self.data.as_mut()) {
107            update(world, data);
108        }
109    }
110}
111
112fn apply_defaults(world: &mut World) {
113    world.resources.render_settings.atmosphere = Atmosphere::Sky;
114    world.resources.debug_draw.show_grid = false;
115    nightshade::plugins::render::install(world);
116    nightshade::plugins::ui::install(world);
117    nightshade::plugins::cutscene::install(world);
118    #[cfg(feature = "physics")]
119    nightshade::plugins::physics::install(world);
120    #[cfg(feature = "navmesh")]
121    nightshade::plugins::navmesh::install(world);
122    #[cfg(feature = "audio")]
123    {
124        #[cfg(not(target_arch = "wasm32"))]
125        nightshade::plugins::audio::systems::initialize_audio_system(world);
126        nightshade::plugins::audio::install(world);
127    }
128    let sun = spawn_sun(world);
129    world.set(sun, Name(SUN_NAME.to_string()));
130    register_named(world, SUN_NAME, sun);
131    load_procedural_textures(world);
132    crate::draw::initialize_draw_pools(world);
133    crate::camera::orbit_camera(world, Vec3::zeros(), 8.0);
134}
135
136fn run_camera_systems(world: &mut World) {
137    let Some(camera) = world.resources.active_camera else {
138        return;
139    };
140    let drives_controllers = world
141        .get::<nightshade::ecs::primitives::Name>(camera)
142        .is_some_and(|name| name.0 == CAMERA_ORBIT || name.0 == CAMERA_FLY);
143    if drives_controllers {
144        camera_controllers_system(world);
145    }
146    #[cfg(feature = "physics")]
147    {
148        let drives_character = world
149            .get::<nightshade::ecs::primitives::Name>(camera)
150            .is_some_and(|name| name.0 == CAMERA_FIRST_PERSON);
151        if drives_character {
152            first_person_camera_look_system(world);
153        }
154    }
155}
156
157/// Runs a program from two closures, handing the engine the main loop.
158///
159/// `setup` runs once after the renderer is ready and returns your state,
160/// anything from a single [`Entity`](crate::prelude::Entity) to a struct of your own. `update` runs
161/// every frame and receives that state back mutably. This is the portable
162/// form: it is the only entry point on wasm, where the browser owns the loop.
163/// On native, prefer [`open`](crate::prelude::open) and
164/// [`frame`](crate::prelude::frame) for straight-line code.
165///
166/// ```ignore
167/// run(
168///     |world| spawn_cube(world, vec3(0.0, 0.5, 0.0)),
169///     |world, cube| {
170///         let step = delta_time(world);
171///         rotate(world, *cube, Vec3::y(), step);
172///     },
173/// )
174/// .unwrap();
175/// ```
176pub fn run<Data: 'static>(
177    setup: impl FnOnce(&mut World) -> Data + 'static,
178    update: impl FnMut(&mut World, &mut Data) + 'static,
179) -> Result<(), Box<dyn std::error::Error>> {
180    let log_guards = nightshade::plugins::log::initialize_logging(
181        "nightshade",
182        &nightshade::plugins::log::LogConfig::default(),
183    );
184    let result = launch(ApiState {
185        setup: Some(Box::new(setup)),
186        update: Some(Box::new(update)),
187        data: None,
188        clears_draw_pools: true,
189        frame_limit: frame_limit_from_environment(),
190        frames_rendered: 0,
191    });
192    drop(log_guards);
193    result
194}
195
196/// Runs a static scene: `setup` once, then the engine just renders it.
197///
198/// The default orbit camera stays interactive, so this is the shortest path
199/// to a scene you can look around in.
200pub fn run_scene(
201    setup: impl FnOnce(&mut World) + 'static,
202) -> Result<(), Box<dyn std::error::Error>> {
203    run(setup, |_, _| {})
204}
205
206/// Folds an array of per-frame update functions into a single update that runs
207/// them in order. This is what [`run!`](crate::run!) expands to; call it directly as
208/// `run(setup, systems([a, b, c]))` if you prefer a plain function. Each entry
209/// is a `fn(&mut World, &mut Data)`, so they are named systems or non-capturing
210/// closures, all sharing the state `setup` returned.
211pub fn systems<Data, const N: usize>(
212    updates: [fn(&mut World, &mut Data); N],
213) -> impl FnMut(&mut World, &mut Data) {
214    move |world, data| {
215        for &update in &updates {
216            update(world, data);
217        }
218    }
219}
220
221/// Runs a program from a setup expression and one or more per-frame update
222/// expressions, the variadic form of [`run`](fn@run). Setup runs once and returns your
223/// state; each update runs every frame, in the order given, and receives that
224/// state. Returns the same `Result` as [`run`](fn@run), so `main` can return it.
225///
226/// ```ignore
227/// fn main() -> Result<(), Box<dyn std::error::Error>> {
228///     run!(
229///         |world| spawn_cube(world, vec3(0.0, 0.5, 0.0)),
230///         |world, cube| rotate(world, *cube, Vec3::y(), delta_time(world)),
231///     )
232/// }
233/// ```
234///
235/// Each update is a `fn(&mut World, &mut Data)` or a non-capturing closure of
236/// that shape, where `Data` is whatever setup returns. They all see the same
237/// state, so a program splits cleanly into named systems:
238///
239/// ```ignore
240/// run!(setup, handle_input, move_player, check_collisions)
241/// ```
242///
243/// For a static scene with no per-frame work, use [`run_scene`] instead.
244#[macro_export]
245macro_rules! run {
246    ($setup:expr, $($update:expr),+ $(,)?) => {
247        $crate::prelude::run($setup, $crate::prelude::systems([$($update),+]))
248    };
249}