bevy_app/plugin_group.rs
1use crate::{App, AppError, Plugin};
2use bevy_utils::{
3 tracing::{debug, warn},
4 TypeIdMap,
5};
6use core::any::TypeId;
7
8/// A macro for generating a well-documented [`PluginGroup`] from a list of [`Plugin`] paths.
9///
10/// Every plugin must implement the [`Default`] trait.
11///
12/// # Example
13///
14/// ```
15/// # use bevy_app::*;
16/// #
17/// # mod velocity {
18/// # use bevy_app::*;
19/// # #[derive(Default)]
20/// # pub struct VelocityPlugin;
21/// # impl Plugin for VelocityPlugin { fn build(&self, _: &mut App) {} }
22/// # }
23/// #
24/// # mod collision {
25/// # pub mod capsule {
26/// # use bevy_app::*;
27/// # #[derive(Default)]
28/// # pub struct CapsuleCollisionPlugin;
29/// # impl Plugin for CapsuleCollisionPlugin { fn build(&self, _: &mut App) {} }
30/// # }
31/// # }
32/// #
33/// # #[derive(Default)]
34/// # pub struct TickratePlugin;
35/// # impl Plugin for TickratePlugin { fn build(&self, _: &mut App) {} }
36/// #
37/// # mod features {
38/// # use bevy_app::*;
39/// # #[derive(Default)]
40/// # pub struct ForcePlugin;
41/// # impl Plugin for ForcePlugin { fn build(&self, _: &mut App) {} }
42/// # }
43/// #
44/// # mod web {
45/// # use bevy_app::*;
46/// # #[derive(Default)]
47/// # pub struct WebCompatibilityPlugin;
48/// # impl Plugin for WebCompatibilityPlugin { fn build(&self, _: &mut App) {} }
49/// # }
50/// #
51/// # mod audio {
52/// # use bevy_app::*;
53/// # #[derive(Default)]
54/// # pub struct AudioPlugins;
55/// # impl PluginGroup for AudioPlugins {
56/// # fn build(self) -> PluginGroupBuilder {
57/// # PluginGroupBuilder::start::<Self>()
58/// # }
59/// # }
60/// # }
61/// #
62/// # mod internal {
63/// # use bevy_app::*;
64/// # #[derive(Default)]
65/// # pub struct InternalPlugin;
66/// # impl Plugin for InternalPlugin { fn build(&self, _: &mut App) {} }
67/// # }
68/// #
69/// plugin_group! {
70/// /// Doc comments and annotations are supported: they will be added to the generated plugin
71/// /// group.
72/// #[derive(Debug)]
73/// pub struct PhysicsPlugins {
74/// // If referencing a plugin within the same module, you must prefix it with a colon `:`.
75/// :TickratePlugin,
76/// // If referencing a plugin within a different module, there must be three colons `:::`
77/// // between the final module and the plugin name.
78/// collision::capsule:::CapsuleCollisionPlugin,
79/// velocity:::VelocityPlugin,
80/// // If you feature-flag a plugin, it will be automatically documented. There can only be
81/// // one automatically documented feature flag, and it must be first. All other
82/// // `#[cfg()]` attributes must be wrapped by `#[custom()]`.
83/// #[cfg(feature = "external_forces")]
84/// features:::ForcePlugin,
85/// // More complicated `#[cfg()]`s and annotations are not supported by automatic doc
86/// // generation, in which case you must wrap it in `#[custom()]`.
87/// #[custom(cfg(target_arch = "wasm32"))]
88/// web:::WebCompatibilityPlugin,
89/// // You can nest `PluginGroup`s within other `PluginGroup`s, you just need the
90/// // `#[plugin_group]` attribute.
91/// #[plugin_group]
92/// audio:::AudioPlugins,
93/// // You can hide plugins from documentation. Due to macro limitations, hidden plugins
94/// // must be last.
95/// #[doc(hidden)]
96/// internal:::InternalPlugin
97/// }
98/// /// You may add doc comments after the plugin group as well. They will be appended after
99/// /// the documented list of plugins.
100/// }
101/// ```
102#[macro_export]
103macro_rules! plugin_group {
104 {
105 $(#[$group_meta:meta])*
106 $vis:vis struct $group:ident {
107 $(
108 $(#[cfg(feature = $plugin_feature:literal)])?
109 $(#[custom($plugin_meta:meta)])*
110 $($plugin_path:ident::)* : $plugin_name:ident
111 ),*
112 $(
113 $(,)?$(
114 #[plugin_group]
115 $(#[cfg(feature = $plugin_group_feature:literal)])?
116 $(#[custom($plugin_group_meta:meta)])*
117 $($plugin_group_path:ident::)* : $plugin_group_name:ident
118 ),+
119 )?
120 $(
121 $(,)?$(
122 #[doc(hidden)]
123 $(#[cfg(feature = $hidden_plugin_feature:literal)])?
124 $(#[custom($hidden_plugin_meta:meta)])*
125 $($hidden_plugin_path:ident::)* : $hidden_plugin_name:ident
126 ),+
127 )?
128
129 $(,)?
130 }
131 $($(#[doc = $post_doc:literal])+)?
132 } => {
133 $(#[$group_meta])*
134 ///
135 $(#[doc = concat!(
136 " - [`", stringify!($plugin_name), "`](" $(, stringify!($plugin_path), "::")*, stringify!($plugin_name), ")"
137 $(, " - with feature `", $plugin_feature, "`")?
138 )])*
139 $($(#[doc = concat!(
140 " - [`", stringify!($plugin_group_name), "`](" $(, stringify!($plugin_group_path), "::")*, stringify!($plugin_group_name), ")"
141 $(, " - with feature `", $plugin_group_feature, "`")?
142 )]),+)?
143 $(
144 ///
145 $(#[doc = $post_doc])+
146 )?
147 $vis struct $group;
148
149 impl $crate::PluginGroup for $group {
150 fn build(self) -> $crate::PluginGroupBuilder {
151 let mut group = $crate::PluginGroupBuilder::start::<Self>();
152
153 $(
154 $(#[cfg(feature = $plugin_feature)])?
155 $(#[$plugin_meta])*
156 {
157 const _: () = {
158 const fn check_default<T: Default>() {}
159 check_default::<$($plugin_path::)*$plugin_name>();
160 };
161
162 group = group.add(<$($plugin_path::)*$plugin_name>::default());
163 }
164 )*
165 $($(
166 $(#[cfg(feature = $plugin_group_feature)])?
167 $(#[$plugin_group_meta])*
168 {
169 const _: () = {
170 const fn check_default<T: Default>() {}
171 check_default::<$($plugin_group_path::)*$plugin_group_name>();
172 };
173
174 group = group.add_group(<$($plugin_group_path::)*$plugin_group_name>::default());
175 }
176 )+)?
177 $($(
178 $(#[cfg(feature = $hidden_plugin_feature)])?
179 $(#[$hidden_plugin_meta])*
180 {
181 const _: () = {
182 const fn check_default<T: Default>() {}
183 check_default::<$($hidden_plugin_path::)*$hidden_plugin_name>();
184 };
185
186 group = group.add(<$($hidden_plugin_path::)*$hidden_plugin_name>::default());
187 }
188 )+)?
189
190 group
191 }
192 }
193 };
194}
195
196/// Combines multiple [`Plugin`]s into a single unit.
197///
198/// If you want an easier, but slightly more restrictive, method of implementing this trait, you
199/// may be interested in the [`plugin_group!`] macro.
200pub trait PluginGroup: Sized {
201 /// Configures the [`Plugin`]s that are to be added.
202 fn build(self) -> PluginGroupBuilder;
203 /// Configures a name for the [`PluginGroup`] which is primarily used for debugging.
204 fn name() -> String {
205 core::any::type_name::<Self>().to_string()
206 }
207 /// Sets the value of the given [`Plugin`], if it exists
208 fn set<T: Plugin>(self, plugin: T) -> PluginGroupBuilder {
209 self.build().set(plugin)
210 }
211}
212
213struct PluginEntry {
214 plugin: Box<dyn Plugin>,
215 enabled: bool,
216}
217
218impl PluginGroup for PluginGroupBuilder {
219 fn build(self) -> PluginGroupBuilder {
220 self
221 }
222}
223
224/// Helper method to get the [`TypeId`] of a value without having to name its type.
225fn type_id_of_val<T: 'static>(_: &T) -> TypeId {
226 TypeId::of::<T>()
227}
228
229/// Facilitates the creation and configuration of a [`PluginGroup`].
230///
231/// Provides a build ordering to ensure that [`Plugin`]s which produce/require a [`Resource`](bevy_ecs::system::Resource)
232/// are built before/after dependent/depending [`Plugin`]s. [`Plugin`]s inside the group
233/// can be disabled, enabled or reordered.
234pub struct PluginGroupBuilder {
235 group_name: String,
236 plugins: TypeIdMap<PluginEntry>,
237 order: Vec<TypeId>,
238}
239
240impl PluginGroupBuilder {
241 /// Start a new builder for the [`PluginGroup`].
242 pub fn start<PG: PluginGroup>() -> Self {
243 Self {
244 group_name: PG::name(),
245 plugins: Default::default(),
246 order: Default::default(),
247 }
248 }
249
250 /// Finds the index of a target [`Plugin`]. Panics if the target's [`TypeId`] is not found.
251 fn index_of<Target: Plugin>(&self) -> usize {
252 let index = self
253 .order
254 .iter()
255 .position(|&ty| ty == TypeId::of::<Target>());
256
257 match index {
258 Some(i) => i,
259 None => panic!(
260 "Plugin does not exist in group: {}.",
261 core::any::type_name::<Target>()
262 ),
263 }
264 }
265
266 // Insert the new plugin as enabled, and removes its previous ordering if it was
267 // already present
268 fn upsert_plugin_state<T: Plugin>(&mut self, plugin: T, added_at_index: usize) {
269 self.upsert_plugin_entry_state(
270 TypeId::of::<T>(),
271 PluginEntry {
272 plugin: Box::new(plugin),
273 enabled: true,
274 },
275 added_at_index,
276 );
277 }
278
279 // Insert the new plugin entry as enabled, and removes its previous ordering if it was
280 // already present
281 fn upsert_plugin_entry_state(
282 &mut self,
283 key: TypeId,
284 plugin: PluginEntry,
285 added_at_index: usize,
286 ) {
287 if let Some(entry) = self.plugins.insert(key, plugin) {
288 if entry.enabled {
289 warn!(
290 "You are replacing plugin '{}' that was not disabled.",
291 entry.plugin.name()
292 );
293 }
294 if let Some(to_remove) = self
295 .order
296 .iter()
297 .enumerate()
298 .find(|(i, ty)| *i != added_at_index && **ty == key)
299 .map(|(i, _)| i)
300 {
301 self.order.remove(to_remove);
302 }
303 }
304 }
305
306 /// Sets the value of the given [`Plugin`], if it exists.
307 ///
308 /// # Panics
309 ///
310 /// Panics if the [`Plugin`] does not exist.
311 pub fn set<T: Plugin>(mut self, plugin: T) -> Self {
312 let entry = self.plugins.get_mut(&TypeId::of::<T>()).unwrap_or_else(|| {
313 panic!(
314 "{} does not exist in this PluginGroup",
315 core::any::type_name::<T>(),
316 )
317 });
318 entry.plugin = Box::new(plugin);
319 self
320 }
321
322 /// Adds the plugin [`Plugin`] at the end of this [`PluginGroupBuilder`]. If the plugin was
323 /// already in the group, it is removed from its previous place.
324 // This is not confusing, clippy!
325 #[expect(
326 clippy::should_implement_trait,
327 reason = "This does not emulate the `+` operator, but is more akin to pushing to a stack."
328 )]
329 pub fn add<T: Plugin>(mut self, plugin: T) -> Self {
330 let target_index = self.order.len();
331 self.order.push(TypeId::of::<T>());
332 self.upsert_plugin_state(plugin, target_index);
333 self
334 }
335
336 /// Adds a [`PluginGroup`] at the end of this [`PluginGroupBuilder`]. If the plugin was
337 /// already in the group, it is removed from its previous place.
338 pub fn add_group(mut self, group: impl PluginGroup) -> Self {
339 let Self {
340 mut plugins, order, ..
341 } = group.build();
342
343 for plugin_id in order {
344 self.upsert_plugin_entry_state(
345 plugin_id,
346 plugins.remove(&plugin_id).unwrap(),
347 self.order.len(),
348 );
349
350 self.order.push(plugin_id);
351 }
352
353 self
354 }
355
356 /// Adds a [`Plugin`] in this [`PluginGroupBuilder`] before the plugin of type `Target`.
357 /// If the plugin was already the group, it is removed from its previous place. There must
358 /// be a plugin of type `Target` in the group or it will panic.
359 pub fn add_before<Target: Plugin>(mut self, plugin: impl Plugin) -> Self {
360 let target_index = self.index_of::<Target>();
361 self.order.insert(target_index, type_id_of_val(&plugin));
362 self.upsert_plugin_state(plugin, target_index);
363 self
364 }
365
366 /// Adds a [`Plugin`] in this [`PluginGroupBuilder`] after the plugin of type `Target`.
367 /// If the plugin was already the group, it is removed from its previous place. There must
368 /// be a plugin of type `Target` in the group or it will panic.
369 pub fn add_after<Target: Plugin>(mut self, plugin: impl Plugin) -> Self {
370 let target_index = self.index_of::<Target>() + 1;
371 self.order.insert(target_index, type_id_of_val(&plugin));
372 self.upsert_plugin_state(plugin, target_index);
373 self
374 }
375
376 /// Enables a [`Plugin`].
377 ///
378 /// [`Plugin`]s within a [`PluginGroup`] are enabled by default. This function is used to
379 /// opt back in to a [`Plugin`] after [disabling](Self::disable) it. If there are no plugins
380 /// of type `T` in this group, it will panic.
381 pub fn enable<T: Plugin>(mut self) -> Self {
382 let plugin_entry = self
383 .plugins
384 .get_mut(&TypeId::of::<T>())
385 .expect("Cannot enable a plugin that does not exist.");
386 plugin_entry.enabled = true;
387 self
388 }
389
390 /// Disables a [`Plugin`], preventing it from being added to the [`App`] with the rest of the
391 /// [`PluginGroup`]. The disabled [`Plugin`] keeps its place in the [`PluginGroup`], so it can
392 /// still be used for ordering with [`add_before`](Self::add_before) or
393 /// [`add_after`](Self::add_after), or it can be [re-enabled](Self::enable). If there are no
394 /// plugins of type `T` in this group, it will panic.
395 pub fn disable<T: Plugin>(mut self) -> Self {
396 let plugin_entry = self
397 .plugins
398 .get_mut(&TypeId::of::<T>())
399 .expect("Cannot disable a plugin that does not exist.");
400 plugin_entry.enabled = false;
401 self
402 }
403
404 /// Consumes the [`PluginGroupBuilder`] and [builds](Plugin::build) the contained [`Plugin`]s
405 /// in the order specified.
406 ///
407 /// # Panics
408 ///
409 /// Panics if one of the plugin in the group was already added to the application.
410 #[track_caller]
411 pub fn finish(mut self, app: &mut App) {
412 for ty in &self.order {
413 if let Some(entry) = self.plugins.remove(ty) {
414 if entry.enabled {
415 debug!("added plugin: {}", entry.plugin.name());
416 if let Err(AppError::DuplicatePlugin { plugin_name }) =
417 app.add_boxed_plugin(entry.plugin)
418 {
419 panic!(
420 "Error adding plugin {} in group {}: plugin was already added in application",
421 plugin_name,
422 self.group_name
423 );
424 }
425 }
426 }
427 }
428 }
429}
430
431/// A plugin group which doesn't do anything. Useful for examples:
432/// ```
433/// # use bevy_app::prelude::*;
434/// use bevy_app::NoopPluginGroup as MinimalPlugins;
435///
436/// fn main(){
437/// App::new().add_plugins(MinimalPlugins).run();
438/// }
439/// ```
440#[doc(hidden)]
441pub struct NoopPluginGroup;
442
443impl PluginGroup for NoopPluginGroup {
444 fn build(self) -> PluginGroupBuilder {
445 PluginGroupBuilder::start::<Self>()
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::PluginGroupBuilder;
452 use crate::{App, NoopPluginGroup, Plugin};
453
454 struct PluginA;
455 impl Plugin for PluginA {
456 fn build(&self, _: &mut App) {}
457 }
458
459 struct PluginB;
460 impl Plugin for PluginB {
461 fn build(&self, _: &mut App) {}
462 }
463
464 struct PluginC;
465 impl Plugin for PluginC {
466 fn build(&self, _: &mut App) {}
467 }
468
469 #[test]
470 fn basic_ordering() {
471 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
472 .add(PluginA)
473 .add(PluginB)
474 .add(PluginC);
475
476 assert_eq!(
477 group.order,
478 vec![
479 core::any::TypeId::of::<PluginA>(),
480 core::any::TypeId::of::<PluginB>(),
481 core::any::TypeId::of::<PluginC>(),
482 ]
483 );
484 }
485
486 #[test]
487 fn add_after() {
488 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
489 .add(PluginA)
490 .add(PluginB)
491 .add_after::<PluginA>(PluginC);
492
493 assert_eq!(
494 group.order,
495 vec![
496 core::any::TypeId::of::<PluginA>(),
497 core::any::TypeId::of::<PluginC>(),
498 core::any::TypeId::of::<PluginB>(),
499 ]
500 );
501 }
502
503 #[test]
504 fn add_before() {
505 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
506 .add(PluginA)
507 .add(PluginB)
508 .add_before::<PluginB>(PluginC);
509
510 assert_eq!(
511 group.order,
512 vec![
513 core::any::TypeId::of::<PluginA>(),
514 core::any::TypeId::of::<PluginC>(),
515 core::any::TypeId::of::<PluginB>(),
516 ]
517 );
518 }
519
520 #[test]
521 fn readd() {
522 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
523 .add(PluginA)
524 .add(PluginB)
525 .add(PluginC)
526 .add(PluginB);
527
528 assert_eq!(
529 group.order,
530 vec![
531 core::any::TypeId::of::<PluginA>(),
532 core::any::TypeId::of::<PluginC>(),
533 core::any::TypeId::of::<PluginB>(),
534 ]
535 );
536 }
537
538 #[test]
539 fn readd_after() {
540 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
541 .add(PluginA)
542 .add(PluginB)
543 .add(PluginC)
544 .add_after::<PluginA>(PluginC);
545
546 assert_eq!(
547 group.order,
548 vec![
549 core::any::TypeId::of::<PluginA>(),
550 core::any::TypeId::of::<PluginC>(),
551 core::any::TypeId::of::<PluginB>(),
552 ]
553 );
554 }
555
556 #[test]
557 fn readd_before() {
558 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
559 .add(PluginA)
560 .add(PluginB)
561 .add(PluginC)
562 .add_before::<PluginB>(PluginC);
563
564 assert_eq!(
565 group.order,
566 vec![
567 core::any::TypeId::of::<PluginA>(),
568 core::any::TypeId::of::<PluginC>(),
569 core::any::TypeId::of::<PluginB>(),
570 ]
571 );
572 }
573
574 #[test]
575 fn add_basic_subgroup() {
576 let group_a = PluginGroupBuilder::start::<NoopPluginGroup>()
577 .add(PluginA)
578 .add(PluginB);
579
580 let group_b = PluginGroupBuilder::start::<NoopPluginGroup>()
581 .add_group(group_a)
582 .add(PluginC);
583
584 assert_eq!(
585 group_b.order,
586 vec![
587 core::any::TypeId::of::<PluginA>(),
588 core::any::TypeId::of::<PluginB>(),
589 core::any::TypeId::of::<PluginC>(),
590 ]
591 );
592 }
593
594 #[test]
595 fn add_conflicting_subgroup() {
596 let group_a = PluginGroupBuilder::start::<NoopPluginGroup>()
597 .add(PluginA)
598 .add(PluginC);
599
600 let group_b = PluginGroupBuilder::start::<NoopPluginGroup>()
601 .add(PluginB)
602 .add(PluginC);
603
604 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
605 .add_group(group_a)
606 .add_group(group_b);
607
608 assert_eq!(
609 group.order,
610 vec![
611 core::any::TypeId::of::<PluginA>(),
612 core::any::TypeId::of::<PluginB>(),
613 core::any::TypeId::of::<PluginC>(),
614 ]
615 );
616 }
617}