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        .res_mut::<nightshade::ecs::entity_registry::EntityRegistry>()
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        .res::<nightshade::ecs::entity_registry::EntityRegistry>()
39        .names
40        .get(name)
41        .copied()
42        .filter(|&entity| {
43            world
44                .get::<nightshade::ecs::primitives::Name>(entity)
45                .is_some()
46        });
47    if cached.is_some() {
48        return cached;
49    }
50    let found = nightshade::ecs::world::commands::find_entity_by_name(world, name)?;
51    register_named(world, name, found);
52    Some(found)
53}
54
55pub(crate) struct ApiState<Data> {
56    pub(crate) setup: Option<SetupFunction<Data>>,
57    pub(crate) update: Option<UpdateFunction<Data>>,
58    pub(crate) data: Option<Data>,
59    pub(crate) clears_draw_pools: bool,
60    pub(crate) frame_limit: Option<u32>,
61    pub(crate) frames_rendered: u32,
62    /// The engine's per-frame stages. [`ApiState`] runs them after its own
63    /// per-frame logic, the way the frame schedule ran after the state
64    /// historically. [`open`](crate::prelude::open) lifts the input reset
65    /// out of them so edge-triggered input survives the frame boundary.
66    pub(crate) stages: freecs::Stages<World>,
67}
68
69/// Builds the per-frame stages for the API loop: the engine defaults plus
70/// the sync systems of the capability plugins the API always drives. The
71/// world-side setup those plugins need (retained-UI visibility, physics
72/// enable, audio device) is applied separately in [`apply_defaults`].
73pub(crate) fn build_api_frame_stages() -> freecs::Stages<World> {
74    let mut stages = nightshade::app::build_default_frame_stages();
75    nightshade::plugins::render::register_frame_systems(&mut stages);
76    nightshade::plugins::ui::register_frame_systems(&mut stages);
77    nightshade::plugins::cutscene::register_frame_systems(&mut stages);
78    #[cfg(feature = "physics")]
79    nightshade::plugins::physics::register_frame_systems(&mut stages);
80    #[cfg(feature = "navmesh")]
81    nightshade::plugins::navmesh::register_frame_systems(&mut stages);
82    #[cfg(feature = "audio")]
83    nightshade::plugins::audio::register_frame_systems(&mut stages);
84    stages
85}
86
87pub(crate) fn frame_limit_from_environment() -> Option<u32> {
88    std::env::var("NIGHTSHADE_API_FRAMES")
89        .ok()
90        .and_then(|value| value.parse().ok())
91}
92
93impl<Data: 'static> State for ApiState<Data> {
94    fn initialize(&mut self, world: &mut World) {
95        apply_defaults(world);
96        if let Some(setup) = self.setup.take() {
97            self.data = Some(setup(world));
98        }
99    }
100
101    fn run_systems(&mut self, world: &mut World) {
102        if let Some(limit) = self.frame_limit {
103            self.frames_rendered += 1;
104            if self.frames_rendered >= limit {
105                world
106                    .res_mut::<nightshade::ecs::window::resources::Window>()
107                    .should_exit = true;
108            }
109        }
110        #[cfg(all(feature = "audio", target_arch = "wasm32"))]
111        nightshade::plugins::audio::systems::lazy_initialize_audio_system(world);
112        #[cfg(feature = "gamepad")]
113        {
114            nightshade::plugins::gamepad::gamepad_input_system(world);
115            let gamepad_events = world
116                .res::<nightshade::plugins::gamepad::Gamepad>()
117                .events
118                .clone();
119            for event in gamepad_events {
120                world
121                    .res_mut::<nightshade::ecs::input::resources::Input>()
122                    .events
123                    .push(nightshade::ecs::input::events::AppEvent::Gamepad(event));
124            }
125        }
126        escape_key_exit_system(world);
127        run_camera_systems(world);
128        if self.clears_draw_pools {
129            crate::draw::clear_draw_pools(world);
130        }
131        if let (Some(update), Some(data)) = (self.update.as_mut(), self.data.as_mut()) {
132            update(world, data);
133        }
134        self.stages.run(world);
135    }
136}
137
138fn apply_defaults(world: &mut World) {
139    world
140        .res_mut::<nightshade::render::config::RenderSettings>()
141        .atmosphere = Atmosphere::Sky;
142    world
143        .res_mut::<nightshade::render::config::DebugDraw>()
144        .show_grid = false;
145    nightshade::plugins::ui::install(world);
146    #[cfg(feature = "physics")]
147    nightshade::plugins::physics::install(world);
148    #[cfg(all(feature = "audio", not(target_arch = "wasm32")))]
149    nightshade::plugins::audio::systems::initialize_audio_system(world);
150    let sun = spawn_sun(world);
151    world.set(sun, Name(SUN_NAME.to_string()));
152    register_named(world, SUN_NAME, sun);
153    load_procedural_textures(world);
154    crate::draw::initialize_draw_pools(world);
155    crate::camera::orbit_camera(world, Vec3::zeros(), 8.0);
156}
157
158fn run_camera_systems(world: &mut World) {
159    let Some(camera) = world
160        .res::<nightshade::ecs::camera::resources::ActiveCamera>()
161        .0
162    else {
163        return;
164    };
165    let drives_controllers = world
166        .get::<nightshade::ecs::primitives::Name>(camera)
167        .is_some_and(|name| name.0 == CAMERA_ORBIT || name.0 == CAMERA_FLY);
168    if drives_controllers {
169        camera_controllers_system(world);
170    }
171    #[cfg(feature = "physics")]
172    {
173        let drives_character = world
174            .get::<nightshade::ecs::primitives::Name>(camera)
175            .is_some_and(|name| name.0 == CAMERA_FIRST_PERSON);
176        if drives_character {
177            first_person_camera_look_system(world);
178        }
179    }
180}
181
182/// Runs a program from two closures, handing the engine the main loop.
183///
184/// `setup` runs once after the renderer is ready and returns your state,
185/// anything from a single [`Entity`](crate::prelude::Entity) to a struct of your own. `update` runs
186/// every frame and receives that state back mutably. This is the portable
187/// form: it is the only entry point on wasm, where the browser owns the loop.
188/// On native, prefer [`open`](crate::prelude::open) and
189/// [`frame`](crate::prelude::frame) for straight-line code.
190///
191/// ```ignore
192/// run(
193///     |world| spawn_cube(world, vec3(0.0, 0.5, 0.0)),
194///     |world, cube| {
195///         let step = delta_time(world);
196///         rotate(world, *cube, Vec3::y(), step);
197///     },
198/// )
199/// .unwrap();
200/// ```
201pub fn run<Data: 'static>(
202    setup: impl FnOnce(&mut World) -> Data + 'static,
203    update: impl FnMut(&mut World, &mut Data) + 'static,
204) -> Result<(), Box<dyn std::error::Error>> {
205    let log_guards = nightshade::plugins::log::initialize_logging(
206        "nightshade",
207        &nightshade::plugins::log::LogConfig::default(),
208    );
209    let result = launch(ApiState {
210        setup: Some(Box::new(setup)),
211        update: Some(Box::new(update)),
212        data: None,
213        clears_draw_pools: true,
214        frame_limit: frame_limit_from_environment(),
215        frames_rendered: 0,
216        stages: build_api_frame_stages(),
217    });
218    drop(log_guards);
219    result
220}
221
222/// Runs a static scene: `setup` once, then the engine just renders it.
223///
224/// The default orbit camera stays interactive, so this is the shortest path
225/// to a scene you can look around in.
226pub fn run_scene(
227    setup: impl FnOnce(&mut World) + 'static,
228) -> Result<(), Box<dyn std::error::Error>> {
229    run(setup, |_, _| {})
230}
231
232/// Folds an array of per-frame update functions into a single update that runs
233/// them in order. This is what [`run!`](crate::run!) expands to; call it directly as
234/// `run(setup, systems([a, b, c]))` if you prefer a plain function. Each entry
235/// is a `fn(&mut World, &mut Data)`, so they are named systems or non-capturing
236/// closures, all sharing the state `setup` returned.
237pub fn systems<Data, const N: usize>(
238    updates: [fn(&mut World, &mut Data); N],
239) -> impl FnMut(&mut World, &mut Data) {
240    move |world, data| {
241        for &update in &updates {
242            update(world, data);
243        }
244    }
245}
246
247/// Runs a program from a setup expression and one or more per-frame update
248/// expressions, the variadic form of [`run`](fn@run). Setup runs once and returns your
249/// state; each update runs every frame, in the order given, and receives that
250/// state. Returns the same `Result` as [`run`](fn@run), so `main` can return it.
251///
252/// ```ignore
253/// fn main() -> Result<(), Box<dyn std::error::Error>> {
254///     run!(
255///         |world| spawn_cube(world, vec3(0.0, 0.5, 0.0)),
256///         |world, cube| rotate(world, *cube, Vec3::y(), delta_time(world)),
257///     )
258/// }
259/// ```
260///
261/// Each update is a `fn(&mut World, &mut Data)` or a non-capturing closure of
262/// that shape, where `Data` is whatever setup returns. They all see the same
263/// state, so a program splits cleanly into named systems:
264///
265/// ```ignore
266/// run!(setup, handle_input, move_player, check_collisions)
267/// ```
268///
269/// For a static scene with no per-frame work, use [`run_scene`] instead.
270#[macro_export]
271macro_rules! run {
272    ($setup:expr, $($update:expr),+ $(,)?) => {
273        $crate::prelude::run($setup, $crate::prelude::systems([$($update),+]))
274    };
275}