Skip to main content

pebble/
app.rs

1use crate::{
2    assets::required::RequiredResources,
3    ecs::{
4        plugin::Plugin,
5        resources::Resources,
6        system::{IntoSystem, System},
7        system_set::IntoSystemSet,
8    },
9};
10use std::collections::BTreeMap;
11
12/// Determines when during a frame a system is executed.
13///
14/// Stages are iterated in the order defined here — [`Startup`](SystemStage::Startup)
15/// runs once during [`App::build`], all others run every [`App::update`] tick.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
17pub enum SystemStage {
18    /// Runs once at startup, before the main loop begins.
19    Startup,
20    /// Upload CPU assets to the GPU backend.
21    AssetSync,
22    /// Upload assets that depend on other GPU assets.
23    AssetSyncDeps,
24    /// Runs before the main update.
25    PreUpdate,
26    /// Main game-logic update.
27    Update,
28    /// Runs after the main update.
29    PostUpdate,
30    /// Prepare rendering data before the render stage.
31    PreRender,
32    /// Issue draw calls.
33    Render,
34    /// Cleanup or post-processing after rendering.
35    PostRender,
36}
37
38/// Callback used to drive the application's main loop.
39///
40/// Set with [`App::set_runner`]. The default runner calls [`App::update`] in
41/// an infinite loop.
42pub type AppRunner = Box<dyn FnOnce(App)>;
43
44/// The central application object.
45///
46/// `App` owns the ECS world, resources, plugins, and systems. The typical
47/// lifecycle is:
48///
49/// 1. Create with [`App::new`].
50/// 2. Register plugins with [`add_plugin`](App::add_plugin).
51/// 3. Call [`build`](App::build) to run all plugin registrations, execute
52///    startup systems, and validate required resources.
53/// 4. Call [`run`](App::run) to hand control to the runner.
54pub struct App {
55    pub(crate) world: hecs::World,
56    pub(crate) resources: Resources,
57    plugins: Vec<Box<dyn Plugin>>,
58    systems: BTreeMap<SystemStage, Vec<Box<dyn System>>>,
59    runner: Option<AppRunner>,
60    pub(crate) required: RequiredResources,
61}
62
63impl Default for App {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69impl App {
70    /// Create a new `App` with an empty world and a default infinite-loop runner.
71    pub fn new() -> Self {
72        let mut world = hecs::World::default();
73        let mut resources = Resources::new(&mut world);
74        resources.insert_resource(&mut world, ());
75
76        Self {
77            world: world,
78            resources: resources,
79            plugins: Vec::new(),
80            systems: BTreeMap::new(),
81            runner: Some(Box::new(|mut app| {
82                loop {
83                    app.update();
84                }
85            })),
86            required: RequiredResources::new(),
87        }
88    }
89
90    /// Queue a plugin to be built during [`build`](App::build).
91    pub fn add_plugin(&mut self, plugin: impl Plugin) -> &mut Self {
92        self.plugins.push(Box::new(plugin));
93        self
94    }
95
96    /// Insert a resource into the world immediately.
97    pub fn add_resource(&mut self, res: impl hecs::Component) -> &mut Self {
98        self.resources.insert_resource(&mut self.world, res);
99        self
100    }
101
102    /// Borrow resource `T`, panicking if it is absent.
103    pub fn get_resource<'a, T: hecs::Component>(&'a self) -> hecs::Ref<'a, T> {
104        self.resources.get_resource(&self.world)
105    }
106
107    /// Mutably borrow resource `T`, panicking if it is absent.
108    pub fn get_resource_mut<'a, T: hecs::Component>(&'a self) -> hecs::RefMut<'a, T> {
109        self.resources.get_resource_mut(&self.world)
110    }
111
112    /// Insert resource `T` only if it is not already present.
113    ///
114    /// Returns `true` if the resource was inserted.
115    pub fn try_insert_resource<T: hecs::Component>(&mut self, res: T) -> bool {
116        self.resources.try_insert(&mut self.world, res)
117    }
118
119    /// Register a single system to run at `stage`.
120    pub fn add_system<Marker>(
121        &mut self,
122        stage: SystemStage,
123        system: impl IntoSystem<Marker> + 'static,
124    ) -> &mut Self {
125        self.systems
126            .entry(stage)
127            .or_default()
128            .push(Box::new(system.into_system()));
129        self
130    }
131
132    /// Register multiple systems to run at `stage`.
133    ///
134    /// Accepts a tuple of systems via [`IntoSystemSet`].
135    pub fn add_systems<Marker>(
136        &mut self,
137        stage: SystemStage,
138        systems: impl IntoSystemSet<Marker>,
139    ) -> &mut Self {
140        let entry = self.systems.entry(stage).or_default();
141        entry.extend(systems.into_system_set());
142        self
143    }
144
145    /// Build all plugins, run startup systems, and validate required resources.
146    ///
147    /// Plugins may register additional plugins during their `build` call; this
148    /// repeats until no new plugins are added, up to a hard limit of 64 passes
149    /// to catch accidental infinite registration cycles.
150    pub fn build(&mut self) -> &mut Self {
151        let mut iterations = 0;
152        const MAX_PLUGIN_BUILD_ITERATIONS: u32 = 64;
153
154        while !self.plugins.is_empty() {
155            iterations += 1;
156            if iterations > MAX_PLUGIN_BUILD_ITERATIONS {
157                panic!(
158                    "App::build() exceeded {MAX_PLUGIN_BUILD_ITERATIONS} plugin-registration passes — \
159                 likely a cycle where plugins keep registering each other. Check for a plugin whose \
160                 build() unconditionally re-adds itself or another plugin that re-adds it."
161                );
162            }
163            let plugins: Vec<_> = self.plugins.drain(..).collect();
164            for plugin in plugins {
165                plugin.build(self);
166            }
167        }
168
169        self.required.validate();
170
171        if let Some(systems) = self.systems.remove(&SystemStage::Startup) {
172            for mut system in systems {
173                system.run(&self.world, &self.resources);
174            }
175            self.resources.get_command_buffer().run_on(&mut self.world);
176        }
177        self
178    }
179
180    /// Run all non-startup systems in stage order, then flush the command buffer.
181    pub fn update(&mut self) {
182        for systems in self.systems.values_mut() {
183            for system in systems {
184                system.run(&self.world, &self.resources);
185            }
186            self.resources.get_command_buffer().run_on(&mut self.world);
187        }
188    }
189
190    /// Replace the default runner with a custom one.
191    ///
192    /// The runner receives ownership of the `App` and is responsible for
193    /// calling [`update`](App::update) at the appropriate cadence (e.g. driven
194    /// by a window event loop).
195    pub fn set_runner<F>(&mut self, runner: F) -> &mut Self
196    where
197        F: FnOnce(App) + 'static,
198    {
199        self.runner = Some(Box::new(runner));
200        self
201    }
202
203    /// Consume the app and hand it to the configured runner.
204    ///
205    /// Panics if no runner has been set.
206    pub fn run(&mut self) {
207        let mut owned_app = std::mem::take(self);
208        let runner = owned_app.runner.take().expect("No runner found!");
209        runner(owned_app);
210    }
211}