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 if entry.enabled {
522 debug!("added plugin: {}", entry.plugin.name());
523 if let Err(AppError::DuplicatePlugin { plugin_name }) =
524 app.add_boxed_plugin(entry.plugin)
525 {
526 panic!(
527 "Error adding plugin {} in group {}: plugin was already added in application",
528 plugin_name,
529 self.group_name
530 );
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};
563
564 struct PluginA;
565 impl Plugin for PluginA {
566 fn build(&self, _: &mut App) {}
567 }
568
569 struct PluginB;
570 impl Plugin for PluginB {
571 fn build(&self, _: &mut App) {}
572 }
573
574 struct PluginC;
575 impl Plugin for PluginC {
576 fn build(&self, _: &mut App) {}
577 }
578
579 #[derive(PartialEq, Debug)]
580 struct PluginWithData(u32);
581 impl Plugin for PluginWithData {
582 fn build(&self, _: &mut App) {}
583 }
584
585 fn get_plugin<T: Debug + 'static>(group: &PluginGroupBuilder, id: TypeId) -> &T {
586 group.plugins[&id]
587 .plugin
588 .as_any()
589 .downcast_ref::<T>()
590 .unwrap()
591 }
592
593 #[test]
594 fn contains() {
595 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
596 .add(PluginA)
597 .add(PluginB);
598
599 assert!(group.contains::<PluginA>());
600 assert!(!group.contains::<PluginC>());
601
602 let group = group.disable::<PluginA>();
603
604 assert!(group.enabled::<PluginB>());
605 assert!(!group.enabled::<PluginA>());
606 }
607
608 #[test]
609 fn basic_ordering() {
610 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
611 .add(PluginA)
612 .add(PluginB)
613 .add(PluginC);
614
615 assert_eq!(
616 group.order,
617 vec![
618 TypeId::of::<PluginA>(),
619 TypeId::of::<PluginB>(),
620 TypeId::of::<PluginC>(),
621 ]
622 );
623 }
624
625 #[test]
626 fn add_before() {
627 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
628 .add(PluginA)
629 .add(PluginB)
630 .add_before::<PluginB>(PluginC);
631
632 assert_eq!(
633 group.order,
634 vec![
635 TypeId::of::<PluginA>(),
636 TypeId::of::<PluginC>(),
637 TypeId::of::<PluginB>(),
638 ]
639 );
640 }
641
642 #[test]
643 fn try_add_before() {
644 let group = PluginGroupBuilder::start::<NoopPluginGroup>().add(PluginA);
645
646 let Ok(group) = group.try_add_before::<PluginA, _>(PluginC) else {
647 panic!("PluginA wasn't in group");
648 };
649
650 assert_eq!(
651 group.order,
652 vec![TypeId::of::<PluginC>(), TypeId::of::<PluginA>(),]
653 );
654
655 assert!(group.try_add_before::<PluginA, _>(PluginC).is_err());
656 }
657
658 #[test]
659 #[should_panic(
660 expected = "Plugin does not exist in group: bevy_app::plugin_group::tests::PluginB."
661 )]
662 fn add_before_nonexistent() {
663 PluginGroupBuilder::start::<NoopPluginGroup>()
664 .add(PluginA)
665 .add_before::<PluginB>(PluginC);
666 }
667
668 #[test]
669 fn add_after() {
670 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
671 .add(PluginA)
672 .add(PluginB)
673 .add_after::<PluginA>(PluginC);
674
675 assert_eq!(
676 group.order,
677 vec![
678 TypeId::of::<PluginA>(),
679 TypeId::of::<PluginC>(),
680 TypeId::of::<PluginB>(),
681 ]
682 );
683 }
684
685 #[test]
686 fn try_add_after() {
687 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
688 .add(PluginA)
689 .add(PluginB);
690
691 let Ok(group) = group.try_add_after::<PluginA, _>(PluginC) else {
692 panic!("PluginA wasn't in group");
693 };
694
695 assert_eq!(
696 group.order,
697 vec![
698 TypeId::of::<PluginA>(),
699 TypeId::of::<PluginC>(),
700 TypeId::of::<PluginB>(),
701 ]
702 );
703
704 assert!(group.try_add_after::<PluginA, _>(PluginC).is_err());
705 }
706
707 #[test]
708 #[should_panic(
709 expected = "Plugin does not exist in group: bevy_app::plugin_group::tests::PluginB."
710 )]
711 fn add_after_nonexistent() {
712 PluginGroupBuilder::start::<NoopPluginGroup>()
713 .add(PluginA)
714 .add_after::<PluginB>(PluginC);
715 }
716
717 #[test]
718 fn add_overwrite() {
719 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
720 .add(PluginA)
721 .add(PluginWithData(0x0F))
722 .add(PluginC);
723
724 let id = TypeId::of::<PluginWithData>();
725 assert_eq!(
726 get_plugin::<PluginWithData>(&group, id),
727 &PluginWithData(0x0F)
728 );
729
730 let group = group.add(PluginWithData(0xA0));
731
732 assert_eq!(
733 get_plugin::<PluginWithData>(&group, id),
734 &PluginWithData(0xA0)
735 );
736 assert_eq!(
737 group.order,
738 vec![
739 TypeId::of::<PluginA>(),
740 TypeId::of::<PluginC>(),
741 TypeId::of::<PluginWithData>(),
742 ]
743 );
744
745 let Ok(group) = group.try_add_before_overwrite::<PluginA, _>(PluginWithData(0x01)) else {
746 panic!("PluginA wasn't in group");
747 };
748 assert_eq!(
749 get_plugin::<PluginWithData>(&group, id),
750 &PluginWithData(0x01)
751 );
752 assert_eq!(
753 group.order,
754 vec![
755 TypeId::of::<PluginWithData>(),
756 TypeId::of::<PluginA>(),
757 TypeId::of::<PluginC>(),
758 ]
759 );
760
761 let Ok(group) = group.try_add_after_overwrite::<PluginA, _>(PluginWithData(0xdeadbeef))
762 else {
763 panic!("PluginA wasn't in group");
764 };
765 assert_eq!(
766 get_plugin::<PluginWithData>(&group, id),
767 &PluginWithData(0xdeadbeef)
768 );
769 assert_eq!(
770 group.order,
771 vec![
772 TypeId::of::<PluginA>(),
773 TypeId::of::<PluginWithData>(),
774 TypeId::of::<PluginC>(),
775 ]
776 );
777 }
778
779 #[test]
780 fn readd() {
781 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
782 .add(PluginA)
783 .add(PluginB)
784 .add(PluginC)
785 .add(PluginB);
786
787 assert_eq!(
788 group.order,
789 vec![
790 TypeId::of::<PluginA>(),
791 TypeId::of::<PluginC>(),
792 TypeId::of::<PluginB>(),
793 ]
794 );
795 }
796
797 #[test]
798 fn readd_before() {
799 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
800 .add(PluginA)
801 .add(PluginB)
802 .add(PluginC)
803 .add_before::<PluginB>(PluginC);
804
805 assert_eq!(
806 group.order,
807 vec![
808 TypeId::of::<PluginA>(),
809 TypeId::of::<PluginC>(),
810 TypeId::of::<PluginB>(),
811 ]
812 );
813 }
814
815 #[test]
816 fn readd_after() {
817 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
818 .add(PluginA)
819 .add(PluginB)
820 .add(PluginC)
821 .add_after::<PluginA>(PluginC);
822
823 assert_eq!(
824 group.order,
825 vec![
826 TypeId::of::<PluginA>(),
827 TypeId::of::<PluginC>(),
828 TypeId::of::<PluginB>(),
829 ]
830 );
831 }
832
833 #[test]
834 fn add_basic_subgroup() {
835 let group_a = PluginGroupBuilder::start::<NoopPluginGroup>()
836 .add(PluginA)
837 .add(PluginB);
838
839 let group_b = PluginGroupBuilder::start::<NoopPluginGroup>()
840 .add_group(group_a)
841 .add(PluginC);
842
843 assert_eq!(
844 group_b.order,
845 vec![
846 TypeId::of::<PluginA>(),
847 TypeId::of::<PluginB>(),
848 TypeId::of::<PluginC>(),
849 ]
850 );
851 }
852
853 #[test]
854 fn add_conflicting_subgroup() {
855 let group_a = PluginGroupBuilder::start::<NoopPluginGroup>()
856 .add(PluginA)
857 .add(PluginC);
858
859 let group_b = PluginGroupBuilder::start::<NoopPluginGroup>()
860 .add(PluginB)
861 .add(PluginC);
862
863 let group = PluginGroupBuilder::start::<NoopPluginGroup>()
864 .add_group(group_a)
865 .add_group(group_b);
866
867 assert_eq!(
868 group.order,
869 vec![
870 TypeId::of::<PluginA>(),
871 TypeId::of::<PluginB>(),
872 TypeId::of::<PluginC>(),
873 ]
874 );
875 }
876}