Skip to main content

pebble/
app.rs

1use crate::{
2    ecs::{
3        events::{AsyncEventChannel, Events, drain_async_events},
4        plugin::Plugin,
5        resources::Resources,
6        system::{IntoSystem, System},
7        system_set::IntoSystemSet,
8    },
9};
10use std::collections::{BTreeMap, BinaryHeap, HashMap};
11
12/// Determines when during a frame a system is executed.
13///
14/// [`Startup`](SystemStage::Startup) runs on every tick until a system on it
15/// returns `Some(())` (then never again). Systems returning `Option<()>` are
16/// automatically "once" — they retry each tick until they succeed.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
18pub enum SystemStage {
19    /// Run-once startup systems — any system added here that returns
20    /// `Option<()>` runs until it returns `Some(())`, then is permanently
21    /// retired.
22    Startup,
23    /// Upload CPU-side source assets to the GPU backend.
24    AssetSync,
25    /// Construct GPU resources and upload assets that depend on other
26    /// processed assets.
27    AssetSyncDeps,
28    /// Before the main update.
29    PreUpdate,
30    /// Main game-logic update.
31    Update,
32    /// After the main update.
33    PostUpdate,
34    /// Prepare rendering data and poll for the GPU backend.
35    PreRender,
36    /// Issue draw calls.
37    Render,
38    /// Cleanup or post-processing after rendering.
39    PostRender,
40}
41
42/// Fixed per-tick order for all stages.
43const ALL_STAGES: [SystemStage; 9] = [
44    SystemStage::Startup,
45    SystemStage::AssetSync,
46    SystemStage::AssetSyncDeps,
47    SystemStage::PreUpdate,
48    SystemStage::Update,
49    SystemStage::PostUpdate,
50    SystemStage::PreRender,
51    SystemStage::Render,
52    SystemStage::PostRender,
53];
54
55/// Callback used to drive the application's main loop.
56///
57/// Set with [`App::set_runner`]. The default runner calls [`App::update`] in
58/// an infinite loop.
59pub type AppRunner = Box<dyn FnOnce(App)>;
60
61/// The central application object.
62///
63/// `App` owns the ECS world, resources, plugins, and systems. The typical
64/// lifecycle is:
65///
66/// 1. Create with [`App::new`].
67/// 2. Register plugins with [`add_plugin`](App::add_plugin).
68/// 3. Call [`build`](App::build) to run all plugin registrations and sort systems.
69/// 4. Call [`run`](App::run) to hand control to the runner.
70pub struct App {
71    pub(crate) world: hecs::World,
72    pub(crate) resources: Resources,
73    plugins: Vec<Box<dyn Plugin>>,
74    systems: BTreeMap<SystemStage, Vec<Box<dyn System>>>,
75    runner: Option<AppRunner>,
76    /// One closure per event type registered via [`add_event`](App::add_event),
77    /// each calling that type's [`Events::update`] to age its buffers. Run
78    /// at the front of every [`update`](App::update) tick, before any user
79    /// system, so a reader anywhere in the tick sees a consistent view.
80    event_updaters: Vec<Box<dyn FnMut(&hecs::World, &Resources)>>,
81}
82
83impl Default for App {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl App {
90    /// Create a new `App` with an empty world and a default infinite-loop runner.
91    ///
92    /// Builds in [`TimePlugin`](crate::time::TimePlugin) — `Res<Time>` works
93    /// without registering anything yourself — along with
94    /// [`GamepadPlugin`](crate::gamepad::GamepadPlugin) (`Res<Gamepads>`) and
95    /// [`AudioPlugin`](crate::audio::AudioPlugin) (`Res<AudioOutput>`).
96    pub fn new() -> Self {
97        let mut world = hecs::World::default();
98        let mut resources = Resources::new(&mut world);
99        resources.insert_resource(&mut world, ());
100
101        let mut app = Self {
102            world: world,
103            resources: resources,
104            plugins: Vec::new(),
105            systems: BTreeMap::new(),
106            runner: Some(Box::new(|mut app| {
107                loop {
108                    app.update();
109                }
110            })),
111            event_updaters: Vec::new(),
112        };
113        app.add_plugin(crate::time::TimePlugin);
114        app.add_plugin(crate::gamepad::GamepadPlugin);
115        app.add_plugin(crate::audio::AudioPlugin);
116        app
117    }
118
119    /// Run every system in `stage` once, then flush the command buffer.
120    fn run_stage_once(&mut self, stage: SystemStage) {
121        if let Some(systems) = self.systems.get_mut(&stage) {
122            for system in systems.iter_mut() {
123                let _guard = crate::ecs::resources::set_current_system(system.name());
124                system.run(&self.world, &self.resources);
125            }
126        }
127        self.resources.get_command_buffer().run_on(&mut self.world);
128    }
129
130    /// Queue a plugin to be built during [`build`](App::build).
131    pub fn add_plugin(&mut self, plugin: impl Plugin) -> &mut Self {
132        self.plugins.push(Box::new(plugin));
133        self
134    }
135
136    /// Insert a resource into the world immediately.
137    pub fn add_resource(&mut self, res: impl hecs::Component) -> &mut Self {
138        self.resources.insert_resource(&mut self.world, res);
139        self
140    }
141
142    /// Borrow resource `T`, panicking if it is absent.
143    pub fn get_resource<'a, T: hecs::Component>(&'a self) -> hecs::Ref<'a, T> {
144        self.resources.get_resource(&self.world)
145    }
146
147    /// Mutably borrow resource `T`, panicking if it is absent.
148    pub fn get_resource_mut<'a, T: hecs::Component>(&'a self) -> hecs::RefMut<'a, T> {
149        self.resources.get_resource_mut(&self.world)
150    }
151
152    /// Insert resource `T` only if it is not already present.
153    ///
154    /// Returns `true` if the resource was inserted.
155    pub fn try_insert_resource<T: hecs::Component>(&mut self, res: T) -> bool {
156        self.resources.try_insert(&mut self.world, res)
157    }
158
159    /// Register event type `T`, making [`EventWriter<T>`](crate::ecs::events::EventWriter)
160    /// and [`EventReader<T>`](crate::ecs::events::EventReader) usable as
161    /// system parameters.
162    pub fn add_event<T: hecs::Component>(&mut self) -> &mut Self {
163        self.try_insert_resource(Events::<T>::default());
164        self.event_updaters.push(Box::new(|world, resources| {
165            resources.get_resource_mut::<Events<T>>(world).update();
166        }));
167        self
168    }
169
170    /// Register event type `T` as in [`add_event`](Self::add_event), and
171    /// additionally make [`AsyncEventWriter<T>`](crate::ecs::events::AsyncEventWriter)
172    /// usable as a system parameter.
173    pub fn add_async_event<T: hecs::Component>(&mut self) -> &mut Self {
174        self.add_event::<T>();
175        self.try_insert_resource(AsyncEventChannel::<T>::new());
176        self.add_system(SystemStage::PreUpdate, drain_async_events::<T>);
177        self
178    }
179
180    /// Register a single system to run at `stage`.
181    pub fn add_system<Marker>(
182        &mut self,
183        stage: SystemStage,
184        system: impl IntoSystem<Marker> + 'static,
185    ) -> &mut Self {
186        self.systems
187            .entry(stage)
188            .or_default()
189            .push(Box::new(system.into_system()));
190        self
191    }
192
193    /// Register multiple systems to run at `stage`.
194    ///
195    /// Accepts a tuple of systems via [`IntoSystemSet`].
196    pub fn add_systems<Marker>(
197        &mut self,
198        stage: SystemStage,
199        systems: impl IntoSystemSet<Marker>,
200    ) -> &mut Self {
201        let entry = self.systems.entry(stage).or_default();
202        entry.extend(systems.into_system_set());
203        self
204    }
205
206    /// Topologically sort `systems` by each system's [`System::after_ids`]/[`System::before_ids`]
207    /// constraints, breaking ties by original registration order.
208    ///
209    /// Panics if the constraints form a cycle.
210    fn sort_stage(stage: SystemStage, systems: &mut Vec<Box<dyn System>>) {
211        let id_index: HashMap<std::any::TypeId, usize> = systems
212            .iter()
213            .enumerate()
214            .map(|(i, s)| (s.ordering_id(), i))
215            .collect();
216
217        let n = systems.len();
218        let mut adjacency: Vec<Vec<usize>> = vec![Vec::new(); n];
219        let mut in_degree = vec![0usize; n];
220
221        for (i, system) in systems.iter().enumerate() {
222            for id in system.after_ids() {
223                if let Some(&dep) = id_index.get(id) {
224                    adjacency[dep].push(i);
225                    in_degree[i] += 1;
226                }
227            }
228            for id in system.before_ids() {
229                if let Some(&dependent) = id_index.get(id) {
230                    adjacency[i].push(dependent);
231                    in_degree[dependent] += 1;
232                }
233            }
234        }
235
236        // Min-heap on original index so ties resolve to registration order.
237        let mut ready: BinaryHeap<std::cmp::Reverse<usize>> = in_degree
238            .iter()
239            .enumerate()
240            .filter(|(_, d)| **d == 0)
241            .map(|(i, _)| std::cmp::Reverse(i))
242            .collect();
243
244        let mut order = Vec::with_capacity(n);
245        while let Some(std::cmp::Reverse(u)) = ready.pop() {
246            order.push(u);
247            for &v in &adjacency[u] {
248                in_degree[v] -= 1;
249                if in_degree[v] == 0 {
250                    ready.push(std::cmp::Reverse(v));
251                }
252            }
253        }
254
255        if order.len() != n {
256            let stuck: Vec<&'static str> = (0..n)
257                .filter(|i| in_degree[*i] > 0)
258                .map(|i| systems[i].name())
259                .collect();
260            panic!(
261                "{stage:?}: system ordering constraints form a cycle among: {stuck:?}"
262            );
263        }
264
265        let mut taken: Vec<Option<Box<dyn System>>> = systems.drain(..).map(Some).collect();
266        for i in order {
267            systems.push(taken[i].take().unwrap());
268        }
269    }
270
271    /// Build all plugins and sort systems.
272    ///
273    /// Plugins may register additional plugins during their `build` call; this
274    /// repeats until no new plugins are added, up to a hard limit of 64 passes
275    /// to catch accidental infinite registration cycles.
276    pub fn build(&mut self) -> &mut Self {
277        let mut iterations = 0;
278        const MAX_PLUGIN_BUILD_ITERATIONS: u32 = 64;
279
280        while !self.plugins.is_empty() {
281            iterations += 1;
282            if iterations > MAX_PLUGIN_BUILD_ITERATIONS {
283                panic!(
284                    "App::build() exceeded {MAX_PLUGIN_BUILD_ITERATIONS} plugin-registration passes — \
285                 likely a cycle where plugins keep registering each other. Check for a plugin whose \
286                 build() unconditionally re-adds itself or another plugin that re-adds it."
287                );
288            }
289            let plugins: Vec<_> = self.plugins.drain(..).collect();
290            for plugin in plugins {
291                plugin.build(self);
292            }
293        }
294
295        for (stage, systems) in self.systems.iter_mut() {
296            Self::sort_stage(*stage, systems);
297        }
298
299        self
300    }
301
302    /// Run every stage once per tick, in [`ALL_STAGES`] order.
303    pub fn update(&mut self) {
304        for updater in self.event_updaters.iter_mut() {
305            updater(&self.world, &self.resources);
306        }
307        for stage in ALL_STAGES {
308            self.run_stage_once(stage);
309        }
310    }
311
312    /// Replace the default runner with a custom one.
313    ///
314    /// The runner receives ownership of the `App` and is responsible for
315    /// calling [`update`](App::update) at the appropriate cadence (e.g. driven
316    /// by a window event loop).
317    pub fn set_runner<F>(&mut self, runner: F) -> &mut Self
318    where
319        F: FnOnce(App) + 'static,
320    {
321        self.runner = Some(Box::new(runner));
322        self
323    }
324
325    /// Consume the app and hand it to the configured runner.
326    ///
327    /// Panics if no runner has been set.
328    pub fn run(&mut self) {
329        let mut owned_app = std::mem::take(self);
330        let runner = owned_app.runner.take().expect("No runner found!");
331        runner(owned_app);
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338    use crate::ecs::system::{ResMut, SystemOrderingExt};
339
340    struct Order(Vec<&'static str>);
341
342    fn sys_a(mut o: ResMut<Order>) {
343        o.0.push("a");
344    }
345    fn sys_b(mut o: ResMut<Order>) {
346        o.0.push("b");
347    }
348    fn sys_c(mut o: ResMut<Order>) {
349        o.0.push("c");
350    }
351
352    #[test]
353    fn systems_run_in_declared_order() {
354        let mut app = App::new();
355        app.add_resource(Order(Vec::new()));
356
357        // Registered in a-b-c order, but both a and b declare they must run
358        // after c — the sort should move c first while leaving a before b
359        // (their relative registration order) intact.
360        app.add_system(SystemStage::Update, sys_a.after(sys_c));
361        app.add_system(SystemStage::Update, sys_b.after(sys_c));
362        app.add_system(SystemStage::Update, sys_c);
363
364        app.build();
365        app.update();
366
367        let order = app.get_resource::<Order>();
368        assert_eq!(order.0, vec!["c", "a", "b"]);
369    }
370
371    #[test]
372    #[should_panic(expected = "cycle")]
373    fn cyclic_ordering_constraints_panic() {
374        let mut app = App::new();
375        app.add_resource(Order(Vec::new()));
376
377        app.add_system(SystemStage::Update, sys_a.after(sys_b));
378        app.add_system(SystemStage::Update, sys_b.after(sys_a));
379
380        app.build();
381    }
382
383    #[test]
384    fn time_plugin_is_already_built_into_a_fresh_app() {
385        let mut app = App::new();
386        app.build();
387
388        // Doesn't panic — `Time` exists without anyone calling
389        // `add_plugin(TimePlugin)` themselves.
390        let _ = app.get_resource::<crate::time::Time>();
391    }
392
393    #[test]
394    fn registering_time_plugin_again_does_not_double_register_its_system() {
395        // Baseline: however many PreUpdate systems App::new()'s own
396        // automatic plugins (Time, Gamepad, ...) register on their own —
397        // not hardcoded, so this stays correct as more get added.
398        let mut baseline = App::new();
399        baseline.build();
400        let baseline_count = baseline.systems.get(&SystemStage::PreUpdate).map_or(0, Vec::len);
401
402        let mut app = App::new();
403        app.add_plugin(crate::time::TimePlugin); // redundant - App::new() already built it in
404        app.build();
405
406        // If TimePlugin weren't idempotent, this would be one more than baseline.
407        let tick_systems = app.systems.get(&SystemStage::PreUpdate).map_or(0, Vec::len);
408        assert_eq!(tick_systems, baseline_count);
409    }
410}