1use crate::{App, AppError, Plugin};
2use alloc::{
3 boxed::Box,
4 string::{String, ToString},
5 vec::Vec,
6};
7use bevy_platform::collections::hash_map::Entry;
8use bevy_utils::TypeIdMap;
9use core::any::TypeId;
10use log::{debug, warn};
11
12#[macro_export]
107macro_rules! plugin_group {
108 {
109 $(#[$group_meta:meta])*
110 $vis:vis struct $group:ident {
111 $(
112 $(#[cfg(feature = $plugin_feature:literal)])?
113 $(#[custom($plugin_meta:meta)])*
114 $($plugin_path:ident::)* : $plugin_name:ident
115 ),*
116 $(
117 $(,)?$(
118 #[plugin_group]
119 $(#[cfg(feature = $plugin_group_feature:literal)])?
120 $(#[custom($plugin_group_meta:meta)])*
121 $($plugin_group_path:ident::)* : $plugin_group_name:ident
122 ),+
123 )?
124 $(
125 $(,)?$(
126 #[doc(hidden)]
127 $(#[cfg(feature = $hidden_plugin_feature:literal)])?
128 $(#[custom($hidden_plugin_meta:meta)])*
129 $($hidden_plugin_path:ident::)* : $hidden_plugin_name:ident
130 ),+
131 )?
132
133 $(,)?
134 }
135 $($(#[doc = $post_doc:literal])+)?
136 } => {
137 $(#[$group_meta])*
138 $(#[doc = concat!(
140 " - [`", stringify!($plugin_name), "`](" $(, stringify!($plugin_path), "::")*, stringify!($plugin_name), ")"
141 $(, " - with feature `", $plugin_feature, "`")?
142 )])*
143 $($(#[doc = concat!(
144 " - [`", stringify!($plugin_group_name), "`](" $(, stringify!($plugin_group_path), "::")*, stringify!($plugin_group_name), ")"
145 $(, " - with feature `", $plugin_group_feature, "`")?
146 )])+)?
147 $(
148 $(#[doc = $post_doc])+
150 )?
151 $vis struct $group;
152
153 impl $crate::PluginGroup for $group {
154 fn build(self) -> $crate::PluginGroupBuilder {
155 let mut group = $crate::PluginGroupBuilder::start::<Self>();
156
157 $(
158 $(#[cfg(feature = $plugin_feature)])?
159 $(#[$plugin_meta])*
160 {
161 const _: () = {
162 const fn check_default<T: Default>() {}
163 check_default::<$($plugin_path::)*$plugin_name>();
164 };
165
166 group = group.add(<$($plugin_path::)*$plugin_name>::default());
167 }
168 )*
169 $($(
170 $(#[cfg(feature = $plugin_group_feature)])?
171 $(#[$plugin_group_meta])*
172 {
173 const _: () = {
174 const fn check_default<T: Default>() {}
175 check_default::<$($plugin_group_path::)*$plugin_group_name>();
176 };
177
178 group = group.add_group(<$($plugin_group_path::)*$plugin_group_name>::default());
179 }
180 )+)?
181 $($(
182 $(#[cfg(feature = $hidden_plugin_feature)])?
183 $(#[$hidden_plugin_meta])*
184 {
185 const _: () = {
186 const fn check_default<T: Default>() {}
187 check_default::<$($hidden_plugin_path::)*$hidden_plugin_name>();
188 };
189
190 group = group.add(<$($hidden_plugin_path::)*$hidden_plugin_name>::default());
191 }
192 )+)?
193
194 group
195 }
196 }
197 };
198}
199
200pub trait PluginGroup: Sized {
205 fn build(self) -> PluginGroupBuilder;
207 fn name() -> String {
209 core::any::type_name::<Self>().to_string()
210 }
211 fn set<T: Plugin>(self, plugin: T) -> PluginGroupBuilder {
213 self.build().set(plugin)
214 }
215}
216
217struct PluginEntry {
218 plugin: Box<dyn Plugin>,
219 enabled: bool,
220}
221
222impl PluginGroup for PluginGroupBuilder {
223 fn build(self) -> PluginGroupBuilder {
224 self
225 }
226}
227
228pub struct PluginGroupBuilder {
234 group_name: String,
235 plugins: TypeIdMap<PluginEntry>,
236 order: Vec<TypeId>,
237}
238
239impl PluginGroupBuilder {
240 pub fn start<PG: PluginGroup>() -> Self {
242 Self {
243 group_name: PG::name(),
244 plugins: Default::default(),
245 order: Default::default(),
246 }
247 }
248
249 pub fn contains<T: Plugin>(&self) -> bool {
251 self.plugins.contains_key(&TypeId::of::<T>())
252 }
253
254 pub fn enabled<T: Plugin>(&self) -> bool {
256 self.plugins
257 .get(&TypeId::of::<T>())
258 .is_some_and(|e| e.enabled)
259 }
260
261 fn index_of<Target: Plugin>(&self) -> Option<usize> {
263 self.order
264 .iter()
265 .position(|&ty| ty == TypeId::of::<Target>())
266 }
267
268 fn upsert_plugin_state<T: Plugin>(&mut self, plugin: T, added_at_index: usize) {
271 self.upsert_plugin_entry_state(
272 TypeId::of::<T>(),
273 PluginEntry {
274 plugin: Box::new(plugin),
275 enabled: true,
276 },
277 added_at_index,
278 );
279 }
280
281 fn upsert_plugin_entry_state(
284 &mut self,
285 key: TypeId,
286 plugin: PluginEntry,
287 added_at_index: usize,
288 ) {
289 if let Some(entry) = self.plugins.insert(key, plugin) {
290 if entry.enabled {
291 warn!(
292 "You are replacing plugin '{}' that was not disabled.",
293 entry.plugin.name()
294 );
295 }
296 if let Some(to_remove) = self
297 .order
298 .iter()
299 .enumerate()
300 .find(|(i, ty)| *i != added_at_index && **ty == key)
301 .map(|(i, _)| i)
302 {
303 self.order.remove(to_remove);
304 }
305 }
306 }
307
308 pub fn set<T: Plugin>(self, plugin: T) -> Self {
314 self.try_set(plugin).unwrap_or_else(|_| {
315 panic!(
316 "{} does not exist in this PluginGroup",
317 core::any::type_name::<T>(),
318 )
319 })
320 }
321
322 pub fn try_set<T: Plugin>(mut self, plugin: T) -> Result<Self, (Self, T)> {
326 match self.plugins.entry(TypeId::of::<T>()) {
327 Entry::Occupied(mut entry) => {
328 entry.get_mut().plugin = Box::new(plugin);
329
330 Ok(self)
331 }
332 Entry::Vacant(_) => Err((self, plugin)),
333 }
334 }
335
336 #[expect(
340 clippy::should_implement_trait,
341 reason = "This does not emulate the `+` operator, but is more akin to pushing to a stack."
342 )]
343 pub fn add<T: Plugin>(mut self, plugin: T) -> Self {
344 let target_index = self.order.len();
345 self.order.push(TypeId::of::<T>());
346 self.upsert_plugin_state(plugin, target_index);
347 self
348 }
349
350 pub fn try_add<T: Plugin>(self, plugin: T) -> Result<Self, (Self, T)> {
354 if self.contains::<T>() {
355 return Err((self, plugin));
356 }
357
358 Ok(self.add(plugin))
359 }
360
361 pub fn add_group(mut self, group: impl PluginGroup) -> Self {
364 let Self {
365 mut plugins, order, ..
366 } = group.build();
367
368 for plugin_id in order {
369 self.upsert_plugin_entry_state(
370 plugin_id,
371 plugins.remove(&plugin_id).unwrap(),
372 self.order.len(),
373 );
374
375 self.order.push(plugin_id);
376 }
377
378 self
379 }
380
381 pub fn add_before<Target: Plugin>(self, plugin: impl Plugin) -> Self {
389 self.try_add_before_overwrite::<Target, _>(plugin)
390 .unwrap_or_else(|_| {
391 panic!(
392 "Plugin does not exist in group: {}.",
393 core::any::type_name::<Target>()
394 )
395 })
396 }
397
398 pub fn try_add_before<Target: Plugin, Insert: Plugin>(
403 self,
404 plugin: Insert,
405 ) -> Result<Self, (Self, Insert)> {
406 if self.contains::<Insert>() {
407 return Err((self, plugin));
408 }
409
410 self.try_add_before_overwrite::<Target, _>(plugin)
411 }
412
413 pub fn try_add_before_overwrite<Target: Plugin, Insert: Plugin>(
419 mut self,
420 plugin: Insert,
421 ) -> Result<Self, (Self, Insert)> {
422 let Some(target_index) = self.index_of::<Target>() else {
423 return Err((self, plugin));
424 };
425
426 self.order.insert(target_index, TypeId::of::<Insert>());
427 self.upsert_plugin_state(plugin, target_index);
428 Ok(self)
429 }
430
431 pub fn add_after<Target: Plugin>(self, plugin: impl Plugin) -> Self {
439 self.try_add_after_overwrite::<Target, _>(plugin)
440 .unwrap_or_else(|_| {
441 panic!(
442 "Plugin does not exist in group: {}.",
443 core::any::type_name::<Target>()
444 )
445 })
446 }
447
448 pub fn try_add_after<Target: Plugin, Insert: Plugin>(
453 self,
454 plugin: Insert,
455 ) -> Result<Self, (Self, Insert)> {
456 if self.contains::<Insert>() {
457 return Err((self, plugin));
458 }
459
460 self.try_add_after_overwrite::<Target, _>(plugin)
461 }
462
463 pub fn try_add_after_overwrite<Target: Plugin, Insert: Plugin>(
469 mut self,
470 plugin: Insert,
471 ) -> Result<Self, (Self, Insert)> {
472 let Some(target_index) = self.index_of::<Target>() else {
473 return Err((self, plugin));
474 };
475
476 let target_index = target_index + 1;
477
478 self.order.insert(target_index, TypeId::of::<Insert>());
479 self.upsert_plugin_state(plugin, target_index);
480 Ok(self)
481 }
482
483 pub fn enable<T: Plugin>(mut self) -> Self {
489 let plugin_entry = self
490 .plugins
491 .get_mut(&TypeId::of::<T>())
492 .expect("Cannot enable a plugin that does not exist.");
493 plugin_entry.enabled = true;
494 self
495 }
496
497 pub fn disable<T: Plugin>(mut self) -> Self {
503 let plugin_entry = self
504 .plugins
505 .get_mut(&TypeId::of::<T>())
506 .expect("Cannot disable a plugin that does not exist.");
507 plugin_entry.enabled = false;
508 self
509 }
510
511 #[track_caller]
518 pub fn finish(mut self, app: &mut App) {
519 for ty in &self.order {
520 if let Some(entry) = self.plugins.remove(ty)
521 && entry.enabled
522 {
523 debug!("added plugin: {}", entry.plugin.name());
524 if let Err(AppError::DuplicatePlugin { plugin_name }) =
525 app.add_boxed_plugin(entry.plugin)
526 {
527 panic!(
528 "Error adding plugin {} in group {}: plugin was already added in application",
529 plugin_name,
530 self.group_name
531 );
532 }
533 }
534 }
535 }
536}
537
538#[doc(hidden)]
548pub struct NoopPluginGroup;
549
550impl PluginGroup for NoopPluginGroup {
551 fn build(self) -> PluginGroupBuilder {
552 PluginGroupBuilder::start::<Self>()
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use alloc::vec;
559 use core::{any::TypeId, fmt::Debug};
560
561 use super::PluginGroupBuilder;
562 use crate::{App, NoopPluginGroup, Plugin, PluginGroup};
563
564 #[derive(Default)]
565 struct PluginA;
566 impl Plugin for PluginA {
567 fn build(&self, _: &mut App) {}
568 }
569
570 #[derive(Default)]
571 struct PluginB;
572 impl Plugin for PluginB {
573 fn build(&self, _: &mut App) {}
574 }
575
576 #[derive(Default)]
577 struct PluginC;
578 impl Plugin for PluginC {
579 fn build(&self, _: &mut App) {}
580 }
581
582 #[derive(PartialEq, Debug)]
583 struct PluginWithData(u32);
584 impl Plugin for PluginWithData {
585 fn build(&self, _: &mut App) {}
586 }
587
588 fn get_plugin<T: Debug + 'static>(group: &PluginGroupBuilder, id: TypeId) -> &T {
589 group.plugins[&id]
590 .plugin
591 .as_any()
592 .downcast_ref::<T>()
593 .unwrap()
594 }
595
596 #[test]
597 fn contains() {
598 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
599 .add(PluginA)
600 .add(PluginB);
601
602 assert!(group.contains::<PluginA>());
603 assert!(!group.contains::<PluginC>());
604
605 let group = group.disable::<PluginA>();
606
607 assert!(group.enabled::<PluginB>());
608 assert!(!group.enabled::<PluginA>());
609 }
610
611 #[test]
612 fn basic_ordering() {
613 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
614 .add(PluginA)
615 .add(PluginB)
616 .add(PluginC);
617
618 assert_eq!(
619 group.order,
620 vec![
621 TypeId::of::<PluginA>(),
622 TypeId::of::<PluginB>(),
623 TypeId::of::<PluginC>(),
624 ]
625 );
626 }
627
628 #[test]
629 fn add_before() {
630 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
631 .add(PluginA)
632 .add(PluginB)
633 .add_before::<PluginB>(PluginC);
634
635 assert_eq!(
636 group.order,
637 vec![
638 TypeId::of::<PluginA>(),
639 TypeId::of::<PluginC>(),
640 TypeId::of::<PluginB>(),
641 ]
642 );
643 }
644
645 #[test]
646 fn try_add_before() {
647 let group = PluginGroupBuilder::start::<NoopPluginGroup>().add(PluginA);
648
649 let Ok(group) = group.try_add_before::<PluginA, _>(PluginC) else {
650 panic!("PluginA wasn't in group");
651 };
652
653 assert_eq!(
654 group.order,
655 vec![TypeId::of::<PluginC>(), TypeId::of::<PluginA>(),]
656 );
657
658 assert!(group.try_add_before::<PluginA, _>(PluginC).is_err());
659 }
660
661 #[test]
662 #[should_panic(
663 expected = "Plugin does not exist in group: bevy_app::plugin_group::tests::PluginB."
664 )]
665 fn add_before_nonexistent() {
666 PluginGroupBuilder::start::<NoopPluginGroup>()
667 .add(PluginA)
668 .add_before::<PluginB>(PluginC);
669 }
670
671 #[test]
672 fn add_after() {
673 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
674 .add(PluginA)
675 .add(PluginB)
676 .add_after::<PluginA>(PluginC);
677
678 assert_eq!(
679 group.order,
680 vec![
681 TypeId::of::<PluginA>(),
682 TypeId::of::<PluginC>(),
683 TypeId::of::<PluginB>(),
684 ]
685 );
686 }
687
688 #[test]
689 fn try_add_after() {
690 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
691 .add(PluginA)
692 .add(PluginB);
693
694 let Ok(group) = group.try_add_after::<PluginA, _>(PluginC) else {
695 panic!("PluginA wasn't in group");
696 };
697
698 assert_eq!(
699 group.order,
700 vec![
701 TypeId::of::<PluginA>(),
702 TypeId::of::<PluginC>(),
703 TypeId::of::<PluginB>(),
704 ]
705 );
706
707 assert!(group.try_add_after::<PluginA, _>(PluginC).is_err());
708 }
709
710 #[test]
711 #[should_panic(
712 expected = "Plugin does not exist in group: bevy_app::plugin_group::tests::PluginB."
713 )]
714 fn add_after_nonexistent() {
715 PluginGroupBuilder::start::<NoopPluginGroup>()
716 .add(PluginA)
717 .add_after::<PluginB>(PluginC);
718 }
719
720 #[test]
721 fn add_overwrite() {
722 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
723 .add(PluginA)
724 .add(PluginWithData(0x0F))
725 .add(PluginC);
726
727 let id = TypeId::of::<PluginWithData>();
728 assert_eq!(
729 get_plugin::<PluginWithData>(&group, id),
730 &PluginWithData(0x0F)
731 );
732
733 let group = group.add(PluginWithData(0xA0));
734
735 assert_eq!(
736 get_plugin::<PluginWithData>(&group, id),
737 &PluginWithData(0xA0)
738 );
739 assert_eq!(
740 group.order,
741 vec![
742 TypeId::of::<PluginA>(),
743 TypeId::of::<PluginC>(),
744 TypeId::of::<PluginWithData>(),
745 ]
746 );
747
748 let Ok(group) = group.try_add_before_overwrite::<PluginA, _>(PluginWithData(0x01)) else {
749 panic!("PluginA wasn't in group");
750 };
751 assert_eq!(
752 get_plugin::<PluginWithData>(&group, id),
753 &PluginWithData(0x01)
754 );
755 assert_eq!(
756 group.order,
757 vec![
758 TypeId::of::<PluginWithData>(),
759 TypeId::of::<PluginA>(),
760 TypeId::of::<PluginC>(),
761 ]
762 );
763
764 let Ok(group) = group.try_add_after_overwrite::<PluginA, _>(PluginWithData(0xdeadbeef))
765 else {
766 panic!("PluginA wasn't in group");
767 };
768 assert_eq!(
769 get_plugin::<PluginWithData>(&group, id),
770 &PluginWithData(0xdeadbeef)
771 );
772 assert_eq!(
773 group.order,
774 vec![
775 TypeId::of::<PluginA>(),
776 TypeId::of::<PluginWithData>(),
777 TypeId::of::<PluginC>(),
778 ]
779 );
780 }
781
782 #[test]
783 fn readd() {
784 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
785 .add(PluginA)
786 .add(PluginB)
787 .add(PluginC)
788 .add(PluginB);
789
790 assert_eq!(
791 group.order,
792 vec![
793 TypeId::of::<PluginA>(),
794 TypeId::of::<PluginC>(),
795 TypeId::of::<PluginB>(),
796 ]
797 );
798 }
799
800 #[test]
801 fn readd_before() {
802 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
803 .add(PluginA)
804 .add(PluginB)
805 .add(PluginC)
806 .add_before::<PluginB>(PluginC);
807
808 assert_eq!(
809 group.order,
810 vec![
811 TypeId::of::<PluginA>(),
812 TypeId::of::<PluginC>(),
813 TypeId::of::<PluginB>(),
814 ]
815 );
816 }
817
818 #[test]
819 fn readd_after() {
820 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
821 .add(PluginA)
822 .add(PluginB)
823 .add(PluginC)
824 .add_after::<PluginA>(PluginC);
825
826 assert_eq!(
827 group.order,
828 vec![
829 TypeId::of::<PluginA>(),
830 TypeId::of::<PluginC>(),
831 TypeId::of::<PluginB>(),
832 ]
833 );
834 }
835
836 #[test]
837 fn add_basic_subgroup() {
838 let group_a = PluginGroupBuilder::start::<NoopPluginGroup>()
839 .add(PluginA)
840 .add(PluginB);
841
842 let group_b = PluginGroupBuilder::start::<NoopPluginGroup>()
843 .add_group(group_a)
844 .add(PluginC);
845
846 assert_eq!(
847 group_b.order,
848 vec![
849 TypeId::of::<PluginA>(),
850 TypeId::of::<PluginB>(),
851 TypeId::of::<PluginC>(),
852 ]
853 );
854 }
855
856 #[test]
857 fn add_conflicting_subgroup() {
858 let group_a = PluginGroupBuilder::start::<NoopPluginGroup>()
859 .add(PluginA)
860 .add(PluginC);
861
862 let group_b = PluginGroupBuilder::start::<NoopPluginGroup>()
863 .add(PluginB)
864 .add(PluginC);
865
866 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
867 .add_group(group_a)
868 .add_group(group_b);
869
870 assert_eq!(
871 group.order,
872 vec![
873 TypeId::of::<PluginA>(),
874 TypeId::of::<PluginB>(),
875 TypeId::of::<PluginC>(),
876 ]
877 );
878 }
879
880 plugin_group! {
881 #[derive(Default)]
882 struct PluginGroupA {
883 :PluginA
884 }
885 }
886 plugin_group! {
887 #[derive(Default)]
888 struct PluginGroupB {
889 :PluginB
890 }
891 }
892 plugin_group! {
893 struct PluginGroupC {
894 :PluginC
895 #[plugin_group]
896 :PluginGroupA,
897 #[plugin_group]
898 :PluginGroupB,
899 }
900 }
901 #[test]
902 fn construct_nested_plugin_groups() {
903 PluginGroupC {}.build();
904 }
905}