Skip to main content

pebble/
app.rs

1use crate::{
2    assets::required::RequiredResources,
3    ecs::{
4        events::{AsyncEventChannel, Events, drain_async_events},
5        plugin::Plugin,
6        resources::Resources,
7        system::{IntoSystem, System},
8        system_set::IntoSystemSet,
9    },
10};
11use std::collections::{BTreeMap, BinaryHeap, HashMap};
12
13/// Determines when during a frame a system is executed.
14///
15/// There's no dedicated "run once at startup" stage — instead, any system on
16/// any stage can be made to run at most once with [`.once()`](crate::ecs::system::OnceExt::once),
17/// which turns "have I already done this" into the function's own return
18/// value (`Some(())` = done, retire; `None` = not ready, try again next
19/// tick) instead of a special stage with its own rules. A `.once()` system
20/// naturally waits as many ticks as it needs to (an async GPU backend, a
21/// `LazyResource` that isn't built yet) using the exact same requirement
22/// checks as every other system on its stage.
23///
24/// [`AssetSync`](SystemStage::AssetSync)/[`AssetSyncDeps`](SystemStage::AssetSyncDeps)
25/// are prioritized: they're re-run to convergence (repeated until a full
26/// pass produces no new resources) at the front of every tick and again
27/// after every other stage, so newly queued asset/resource work is drained
28/// before gameplay stages continue rather than waiting for the next tick's
29/// front pass. All other stages run once per [`App::update`] tick, in the
30/// order declared below.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
32pub enum SystemStage {
33    /// Before the main update.
34    PreUpdate,
35    /// Main game-logic update.
36    Update,
37    /// After the main update.
38    PostUpdate,
39    /// Prepare rendering data and poll for the GPU backend.
40    /// The backend resource becomes available here on the tick it finishes
41    /// initialising, making it visible to the asset sync stages.
42    PreRender,
43    /// Upload CPU-side source assets to the GPU backend.
44    AssetSync,
45    /// Construct lazy GPU resources and upload assets that depend on other
46    /// processed assets. Runs in a convergence loop so dependency chains
47    /// (e.g. LazyResource A → LazyResource B) resolve within a single tick.
48    AssetSyncDeps,
49    /// Issue draw calls.
50    Render,
51    /// Cleanup or post-processing after rendering.
52    PostRender,
53}
54
55impl SystemStage {
56    /// Returns `true` for stages that are prioritized and re-run until a
57    /// full pass produces no new resources, instead of running once in
58    /// their declared position in the tick order. See the type-level docs
59    /// on [`SystemStage`].
60    pub fn is_convergent(self) -> bool {
61        matches!(self, Self::AssetSync | Self::AssetSyncDeps)
62    }
63}
64
65/// Fixed per-tick order for every stage *except* the convergent ones
66/// (`AssetSync`, `AssetSyncDeps`), which are driven separately by
67/// [`App::reconverge`] — at the front of the tick and again after each of
68/// these — rather than appearing in this list.
69const TICK_STAGES: [SystemStage; 6] = [
70    SystemStage::PreUpdate,
71    SystemStage::Update,
72    SystemStage::PostUpdate,
73    SystemStage::PreRender,
74    SystemStage::Render,
75    SystemStage::PostRender,
76];
77
78/// Whether a system is safe to run right now, given its declared
79/// [`System::requires`]. See [`App::check_readiness`].
80enum Readiness {
81    /// No unmet requirement — go ahead and run it.
82    Ready,
83    /// Missing a resource that some plugin has declared (via
84    /// [`RequiredResources::provides`]) it eventually provides — wait
85    /// quietly, no error, and try again next pass/tick.
86    WaitingOnLazy,
87    /// Missing a resource nothing has ever declared it will provide —
88    /// almost certainly a genuine oversight, not a timing issue.
89    MissingUnprovided {
90        system: &'static str,
91        resource: &'static str,
92        hint: Option<&'static str>,
93    },
94}
95
96/// Callback used to drive the application's main loop.
97///
98/// Set with [`App::set_runner`]. The default runner calls [`App::update`] in
99/// an infinite loop.
100pub type AppRunner = Box<dyn FnOnce(App)>;
101
102/// The central application object.
103///
104/// `App` owns the ECS world, resources, plugins, and systems. The typical
105/// lifecycle is:
106///
107/// 1. Create with [`App::new`].
108/// 2. Register plugins with [`add_plugin`](App::add_plugin).
109/// 3. Call [`build`](App::build) to run all plugin registrations, execute
110///    validate required resources, and settle `AssetSync`/`AssetSyncDeps`
111///    as far as they can go synchronously.
112/// 4. Call [`run`](App::run) to hand control to the runner.
113pub struct App {
114    pub(crate) world: hecs::World,
115    pub(crate) resources: Resources,
116    plugins: Vec<Box<dyn Plugin>>,
117    systems: BTreeMap<SystemStage, Vec<Box<dyn System>>>,
118    runner: Option<AppRunner>,
119    pub(crate) required: RequiredResources,
120    /// One closure per event type registered via [`add_event`](App::add_event),
121    /// each calling that type's [`Events::update`] to age its buffers. Run
122    /// at the front of every [`update`](App::update) tick, before any user
123    /// system, so a reader anywhere in the tick sees a consistent view. Kept
124    /// here rather than as regular systems because they must run before
125    /// every stage, not just one, and ordering that generically against
126    /// arbitrary user systems isn't worth the complexity.
127    event_updaters: Vec<Box<dyn FnMut(&hecs::World, &Resources)>>,
128}
129
130impl Default for App {
131    fn default() -> Self {
132        Self::new()
133    }
134}
135
136impl App {
137    /// Create a new `App` with an empty world and a default infinite-loop runner.
138    ///
139    /// Builds in [`TimePlugin`](crate::time::TimePlugin) — `Res<Time>` works
140    /// without registering anything yourself — along with
141    /// [`GamepadPlugin`](crate::gamepad::GamepadPlugin) (`Res<Gamepads>`) and
142    /// [`AudioPlugin`](crate::audio::AudioPlugin) (`Res<AudioOutput>`), the
143    /// same way. Both degrade gracefully if the platform has no gamepad
144    /// backend / no audio output device at all (logs and leaves the
145    /// resource absent — `Option<Res<Gamepads>>`/`Option<Res<AudioOutput>>`
146    /// in systems that need to keep working either way); a gamepad backend
147    /// with zero controllers plugged in, or an audio device that's simply
148    /// never played to, are both the ordinary, fully-supported case.
149    pub fn new() -> Self {
150        let mut world = hecs::World::default();
151        let mut resources = Resources::new(&mut world);
152        resources.insert_resource(&mut world, ());
153
154        let mut app = Self {
155            world: world,
156            resources: resources,
157            plugins: Vec::new(),
158            systems: BTreeMap::new(),
159            runner: Some(Box::new(|mut app| {
160                loop {
161                    app.update();
162                }
163            })),
164            required: RequiredResources::new(),
165            event_updaters: Vec::new(),
166        };
167        app.add_plugin(crate::time::TimePlugin);
168        app.add_plugin(crate::gamepad::GamepadPlugin);
169        app.add_plugin(crate::audio::AudioPlugin);
170        app
171    }
172
173    /// Check `system` against `required` without running it. See
174    /// [`Readiness`]. Used by [`run_stage_once`](App::run_stage_once) for
175    /// every stage.
176    ///
177    /// A free function (rather than a `&self` method) so it only borrows
178    /// `world`/`resources`/`required` — the specific fields still available
179    /// while a caller holds a `&mut` borrow of `self.systems` to iterate the
180    /// very system being checked.
181    fn check_readiness(
182        world: &hecs::World,
183        resources: &Resources,
184        required: &RequiredResources,
185        system: &dyn System,
186    ) -> Readiness {
187        for req in system.requires() {
188            if (req.present)(world, resources) {
189                continue;
190            }
191            if required.is_provided(req.type_id) {
192                return Readiness::WaitingOnLazy;
193            }
194            return Readiness::MissingUnprovided {
195                system: system.name(),
196                resource: req.name,
197                hint: req.hint,
198            };
199        }
200        Readiness::Ready
201    }
202
203    /// The advice appended to a "missing resource" panic when the
204    /// [`RequiredResource`](crate::ecs::system::RequiredResource) didn't
205    /// supply its own more specific `hint` — the generic fallback,
206    /// appropriate for a plain `Res<T>`/`ResMut<T>` on an arbitrary
207    /// resource type with no dedicated registration method of its own.
208    fn generic_missing_resource_hint(resource: &'static str) -> String {
209        format!(
210            "If `{resource}` genuinely arrives later (an async backend, a LazyResource, \
211             an Asset upload), call `app.required.provides::<{resource}>()` in whichever \
212             plugin inserts it, and this will wait instead of erroring. Otherwise, insert \
213             it via App::add_resource before this stage runs."
214        )
215    }
216
217    /// Panic with a message naming both the offending system and resource,
218    /// plus either its param-specific `hint` (e.g. "call `app.add_event::<T>()`")
219    /// or, absent that, the generic fallback advice.
220    fn panic_missing_unprovided(
221        stage: SystemStage,
222        system: &'static str,
223        resource: &'static str,
224        hint: Option<&'static str>,
225    ) -> ! {
226        let advice = hint
227            .map(str::to_string)
228            .unwrap_or_else(|| Self::generic_missing_resource_hint(resource));
229        panic!(
230            "{stage:?}: system `{system}` requires `{resource}`, which nothing has \
231             registered as provided.\n\n{advice}"
232        );
233    }
234
235    /// Pre-flight check, run once at the end of [`build`](Self::build): walk
236    /// every registered system in every stage and evaluate its
237    /// [`System::requires`] via [`check_readiness`](Self::check_readiness),
238    /// the same logic [`run_stage_once`](Self::run_stage_once) applies lazily
239    /// as each stage actually runs. A system waiting on a resource that
240    /// something else has [declared it provides](RequiredResources::provides)
241    /// is left alone — it'll show up once that plugin's async/lazy work
242    /// settles. A system requiring a resource that *nothing* provides and
243    /// that isn't already present is a genuine configuration mistake, and
244    /// every such mistake across the whole app is collected into one panic
245    /// here — instead of each one surfacing separately, one at a time, the
246    /// first time its particular stage happens to run.
247    fn validate_requirements(&self) {
248        let mut missing = Vec::new();
249
250        for (stage, systems) in self.systems.iter() {
251            for system in systems.iter() {
252                if let Readiness::MissingUnprovided { system, resource, hint } =
253                    Self::check_readiness(&self.world, &self.resources, &self.required, system.as_ref())
254                {
255                    missing.push((*stage, system, resource, hint));
256                }
257            }
258        }
259
260        if missing.is_empty() {
261            return;
262        }
263
264        let mut message = String::from(
265            "Pebble startup validation failed — the following systems require resources \
266             that nothing has registered as provided:\n",
267        );
268        for (stage, system, resource, hint) in &missing {
269            let advice = hint
270                .map(str::to_string)
271                .unwrap_or_else(|| Self::generic_missing_resource_hint(resource));
272            message.push_str(&format!("\n{stage:?}: system `{system}` requires `{resource}`\n  {advice}\n"));
273        }
274        panic!("{message}");
275    }
276
277    /// Run every system in `stage` once, flush the command buffer, and return
278    /// `true` if any resource was newly inserted during this pass.
279    ///
280    /// A system with an unmet hard [`Res`](crate::ecs::system::Res)/[`ResMut`](crate::ecs::system::ResMut)
281    /// requirement is skipped for this pass if the resource is registered as
282    /// [provided](RequiredResources::provides) somewhere (it'll get there —
283    /// just not yet), or panics immediately, naming the system and resource,
284    /// if nothing ever declared it would provide that resource at all.
285    ///
286    /// [`Commands::insert_resource`](crate::ecs::system::Commands::insert_resource)
287    /// bumps the generation counter at queue time, so both direct inserts and
288    /// deferred command-buffer inserts are detected here with no world
289    /// introspection needed after the flush.
290    fn run_stage_once(&mut self, stage: SystemStage) -> bool {
291        let gen_before = self.resources.generation();
292
293        if let Some(systems) = self.systems.get_mut(&stage) {
294            for system in systems.iter_mut() {
295                match Self::check_readiness(&self.world, &self.resources, &self.required, system.as_ref()) {
296                    Readiness::Ready => {}
297                    Readiness::WaitingOnLazy => continue,
298                    Readiness::MissingUnprovided { system, resource, hint } => {
299                        Self::panic_missing_unprovided(stage, system, resource, hint)
300                    }
301                }
302                let _guard = crate::ecs::resources::set_current_system(system.name());
303                system.run(&self.world, &self.resources);
304            }
305        }
306        self.resources.get_command_buffer().run_on(&mut self.world);
307
308        self.resources.generation() != gen_before
309    }
310
311    /// Run `AssetSync`, then `AssetSyncDeps`, repeating both until a full
312    /// pass produces no new resources, up to `max_passes`. Logs a warning if
313    /// the limit is reached — that usually means a [`LazyResource`](crate::assets::singleton_asset::LazyResource)
314    /// whose `construct()` or an [`Asset`](crate::assets::upload::Asset)
315    /// whose `upload()` always returns `None`.
316    ///
317    /// Called at the front of every tick and again after every stage in
318    /// [`update`](App::update) (and once during [`build`](App::build)), so
319    /// newly-queued asset/resource work is drained immediately instead of
320    /// waiting for the next tick's front pass.
321    fn reconverge(&mut self, max_passes: u32) {
322        for pass in 0..max_passes {
323            let gen_before = self.resources.generation();
324
325            self.run_stage_once(SystemStage::AssetSync);
326            self.run_stage_once(SystemStage::AssetSyncDeps);
327
328            if self.resources.generation() == gen_before {
329                return;
330            }
331            if pass == max_passes - 1 {
332                tracing::warn!(
333                    "AssetSync/AssetSyncDeps did not settle after {max_passes} passes — a \
334                     dependency may be permanently unsatisfiable. Check for a LazyResource \
335                     whose construct() or an Asset whose upload() always returns None."
336                );
337            }
338        }
339    }
340
341    /// Queue a plugin to be built during [`build`](App::build).
342    pub fn add_plugin(&mut self, plugin: impl Plugin) -> &mut Self {
343        self.plugins.push(Box::new(plugin));
344        self
345    }
346
347    /// Insert a resource into the world immediately.
348    pub fn add_resource(&mut self, res: impl hecs::Component) -> &mut Self {
349        self.resources.insert_resource(&mut self.world, res);
350        self
351    }
352
353    /// Borrow resource `T`, panicking if it is absent.
354    pub fn get_resource<'a, T: hecs::Component>(&'a self) -> hecs::Ref<'a, T> {
355        self.resources.get_resource(&self.world)
356    }
357
358    /// Mutably borrow resource `T`, panicking if it is absent.
359    pub fn get_resource_mut<'a, T: hecs::Component>(&'a self) -> hecs::RefMut<'a, T> {
360        self.resources.get_resource_mut(&self.world)
361    }
362
363    /// Insert resource `T` only if it is not already present.
364    ///
365    /// Returns `true` if the resource was inserted.
366    pub fn try_insert_resource<T: hecs::Component>(&mut self, res: T) -> bool {
367        self.resources.try_insert(&mut self.world, res)
368    }
369
370    /// Declare that resource type `T` is expected to be inserted later —
371    /// possibly asynchronously (a background thread's result, a hand-rolled
372    /// lazy resource) rather than up front. A system elsewhere with a hard
373    /// `Res<T>`/`ResMut<T>` requirement on `T` will then wait quietly for it
374    /// instead of `App` treating the absence as a configuration mistake and
375    /// panicking.
376    ///
377    /// [`GraphicsPlugin`](crate::rendering::graphics_plugin::GraphicsPlugin)
378    /// and [`LazyResourcePlugin`](crate::assets::singleton_asset::LazyResourcePlugin)
379    /// already call this for the backend and lazy resource types they
380    /// manage — reach for this directly only for your own resource types
381    /// that arrive outside of those.
382    pub fn provides<T: 'static>(&mut self) -> &mut Self {
383        self.required.provides::<T>();
384        self
385    }
386
387    /// Register event type `T`, making [`EventWriter<T>`](crate::ecs::events::EventWriter)
388    /// and [`EventReader<T>`](crate::ecs::events::EventReader) usable as
389    /// system parameters.
390    ///
391    /// Inserts the backing [`Events<T>`] resource (a no-op if `T` was
392    /// already registered) and schedules its per-tick aging, which is what
393    /// gives events sent during tick `N` a consistent two-tick lifetime —
394    /// visible for the rest of `N` and all of `N + 1` — regardless of which
395    /// stage the writer or reader runs in.
396    pub fn add_event<T: hecs::Component>(&mut self) -> &mut Self {
397        self.try_insert_resource(Events::<T>::default());
398        self.event_updaters.push(Box::new(|world, resources| {
399            resources.get_resource_mut::<Events<T>>(world).update();
400        }));
401        self
402    }
403
404    /// Register event type `T` as in [`add_event`](Self::add_event), and
405    /// additionally make [`AsyncEventWriter<T>`](crate::ecs::events::AsyncEventWriter)
406    /// usable as a system parameter — the friendly way to turn a background
407    /// task's result into a `T` event once it resolves, instead of hand-
408    /// rolling a pending-task resource and poll system yourself.
409    ///
410    /// Requires [`BackgroundTasksPlugin`](crate::threading::BackgroundTasksPlugin)
411    /// to be registered before any system using `AsyncEventWriter<T>` runs —
412    /// that's what [`AsyncEventWriter::spawn`](crate::ecs::events::AsyncEventWriter::spawn)
413    /// drives the future through.
414    pub fn add_async_event<T: hecs::Component>(&mut self) -> &mut Self {
415        self.add_event::<T>();
416        self.try_insert_resource(AsyncEventChannel::<T>::new());
417        self.add_system(SystemStage::PreUpdate, drain_async_events::<T>);
418        self
419    }
420
421    /// Register a single system to run at `stage`.
422    pub fn add_system<Marker>(
423        &mut self,
424        stage: SystemStage,
425        system: impl IntoSystem<Marker> + 'static,
426    ) -> &mut Self {
427        self.systems
428            .entry(stage)
429            .or_default()
430            .push(Box::new(system.into_system()));
431        self
432    }
433
434    /// Register multiple systems to run at `stage`.
435    ///
436    /// Accepts a tuple of systems via [`IntoSystemSet`].
437    pub fn add_systems<Marker>(
438        &mut self,
439        stage: SystemStage,
440        systems: impl IntoSystemSet<Marker>,
441    ) -> &mut Self {
442        let entry = self.systems.entry(stage).or_default();
443        entry.extend(systems.into_system_set());
444        self
445    }
446
447    /// Topologically sort `systems` by each system's [`System::after_ids`]/[`System::before_ids`]
448    /// constraints (referencing other systems' [`System::ordering_id`] within
449    /// the same stage), breaking ties by original registration order.
450    ///
451    /// Panics if the constraints form a cycle, naming every system still
452    /// stuck once no more zero-dependency systems remain.
453    fn sort_stage(stage: SystemStage, systems: &mut Vec<Box<dyn System>>) {
454        let id_index: HashMap<std::any::TypeId, usize> = systems
455            .iter()
456            .enumerate()
457            .map(|(i, s)| (s.ordering_id(), i))
458            .collect();
459
460        let n = systems.len();
461        let mut adjacency: Vec<Vec<usize>> = vec![Vec::new(); n];
462        let mut in_degree = vec![0usize; n];
463
464        for (i, system) in systems.iter().enumerate() {
465            for id in system.after_ids() {
466                if let Some(&dep) = id_index.get(id) {
467                    adjacency[dep].push(i);
468                    in_degree[i] += 1;
469                }
470            }
471            for id in system.before_ids() {
472                if let Some(&dependent) = id_index.get(id) {
473                    adjacency[i].push(dependent);
474                    in_degree[dependent] += 1;
475                }
476            }
477        }
478
479        // Min-heap on original index so ties resolve to registration order.
480        let mut ready: BinaryHeap<std::cmp::Reverse<usize>> = in_degree
481            .iter()
482            .enumerate()
483            .filter(|(_, d)| **d == 0)
484            .map(|(i, _)| std::cmp::Reverse(i))
485            .collect();
486
487        let mut order = Vec::with_capacity(n);
488        while let Some(std::cmp::Reverse(u)) = ready.pop() {
489            order.push(u);
490            for &v in &adjacency[u] {
491                in_degree[v] -= 1;
492                if in_degree[v] == 0 {
493                    ready.push(std::cmp::Reverse(v));
494                }
495            }
496        }
497
498        if order.len() != n {
499            let stuck: Vec<&'static str> = (0..n)
500                .filter(|i| in_degree[*i] > 0)
501                .map(|i| systems[i].name())
502                .collect();
503            panic!(
504                "{stage:?}: system ordering constraints form a cycle among: {stuck:?}"
505            );
506        }
507
508        let mut taken: Vec<Option<Box<dyn System>>> = systems.drain(..).map(Some).collect();
509        for i in order {
510            systems.push(taken[i].take().unwrap());
511        }
512    }
513
514    /// Build all plugins and validate required resources.
515    ///
516    /// Plugins may register additional plugins during their `build` call; this
517    /// repeats until no new plugins are added, up to a hard limit of 64 passes
518    /// to catch accidental infinite registration cycles.
519    pub fn build(&mut self) -> &mut Self {
520        let mut iterations = 0;
521        const MAX_PLUGIN_BUILD_ITERATIONS: u32 = 64;
522
523        while !self.plugins.is_empty() {
524            iterations += 1;
525            if iterations > MAX_PLUGIN_BUILD_ITERATIONS {
526                panic!(
527                    "App::build() exceeded {MAX_PLUGIN_BUILD_ITERATIONS} plugin-registration passes — \
528                 likely a cycle where plugins keep registering each other. Check for a plugin whose \
529                 build() unconditionally re-adds itself or another plugin that re-adds it."
530                );
531            }
532            let plugins: Vec<_> = self.plugins.drain(..).collect();
533            for plugin in plugins {
534                plugin.build(self);
535            }
536        }
537
538        for (stage, systems) in self.systems.iter_mut() {
539            Self::sort_stage(*stage, systems);
540        }
541
542        // Resolve as much as possible synchronously (headless/CPU-only
543        // backends, tests) so resources are ready immediately after
544        // build(). Anything still pending (an async GPU backend, say)
545        // keeps getting retried every tick by update().
546        self.reconverge(64);
547
548        self.validate_requirements();
549
550        self
551    }
552
553    /// Run every stage once per tick, in [`TICK_STAGES`] order. Before every
554    /// tick, and again after every stage, [`reconverge`](App::reconverge)
555    /// drains `AssetSync`/`AssetSyncDeps` — so newly-queued asset or
556    /// resource work is handled immediately rather than waiting for the
557    /// next tick's front pass.
558    pub fn update(&mut self) {
559        for updater in self.event_updaters.iter_mut() {
560            updater(&self.world, &self.resources);
561        }
562
563        self.reconverge(64);
564
565        for stage in TICK_STAGES {
566            self.run_stage_once(stage);
567            self.reconverge(64);
568        }
569    }
570
571    /// Replace the default runner with a custom one.
572    ///
573    /// The runner receives ownership of the `App` and is responsible for
574    /// calling [`update`](App::update) at the appropriate cadence (e.g. driven
575    /// by a window event loop).
576    pub fn set_runner<F>(&mut self, runner: F) -> &mut Self
577    where
578        F: FnOnce(App) + 'static,
579    {
580        self.runner = Some(Box::new(runner));
581        self
582    }
583
584    /// Consume the app and hand it to the configured runner.
585    ///
586    /// Panics if no runner has been set.
587    pub fn run(&mut self) {
588        let mut owned_app = std::mem::take(self);
589        let runner = owned_app.runner.take().expect("No runner found!");
590        runner(owned_app);
591    }
592}
593
594#[cfg(test)]
595mod tests {
596    use super::*;
597    use crate::ecs::system::{ResMut, SystemOrderingExt};
598
599    struct Order(Vec<&'static str>);
600
601    fn sys_a(mut o: ResMut<Order>) {
602        o.0.push("a");
603    }
604    fn sys_b(mut o: ResMut<Order>) {
605        o.0.push("b");
606    }
607    fn sys_c(mut o: ResMut<Order>) {
608        o.0.push("c");
609    }
610
611    #[test]
612    fn systems_run_in_declared_order() {
613        let mut app = App::new();
614        app.add_resource(Order(Vec::new()));
615
616        // Registered in a-b-c order, but both a and b declare they must run
617        // after c — the sort should move c first while leaving a before b
618        // (their relative registration order) intact.
619        app.add_system(SystemStage::Update, sys_a.after(sys_c));
620        app.add_system(SystemStage::Update, sys_b.after(sys_c));
621        app.add_system(SystemStage::Update, sys_c);
622
623        app.build();
624        app.update();
625
626        let order = app.get_resource::<Order>();
627        assert_eq!(order.0, vec!["c", "a", "b"]);
628    }
629
630    #[test]
631    #[should_panic(expected = "cycle")]
632    fn cyclic_ordering_constraints_panic() {
633        let mut app = App::new();
634        app.add_resource(Order(Vec::new()));
635
636        app.add_system(SystemStage::Update, sys_a.after(sys_b));
637        app.add_system(SystemStage::Update, sys_b.after(sys_a));
638
639        app.build();
640    }
641
642    #[test]
643    fn time_plugin_is_already_built_into_a_fresh_app() {
644        let mut app = App::new();
645        app.build();
646
647        // Doesn't panic — `Time` exists without anyone calling
648        // `add_plugin(TimePlugin)` themselves.
649        let _ = app.get_resource::<crate::time::Time>();
650    }
651
652    #[test]
653    fn registering_time_plugin_again_does_not_double_register_its_system() {
654        // Baseline: however many PreUpdate systems App::new()'s own
655        // automatic plugins (Time, Gamepad, ...) register on their own —
656        // not hardcoded, so this stays correct as more get added.
657        let mut baseline = App::new();
658        baseline.build();
659        let baseline_count = baseline.systems.get(&SystemStage::PreUpdate).map_or(0, Vec::len);
660
661        let mut app = App::new();
662        app.add_plugin(crate::time::TimePlugin); // redundant - App::new() already built it in
663        app.build();
664
665        // If TimePlugin weren't idempotent, this would be one more than baseline.
666        let tick_systems = app.systems.get(&SystemStage::PreUpdate).map_or(0, Vec::len);
667        assert_eq!(tick_systems, baseline_count);
668    }
669}