bevy_hierarchy/
valid_parent_check_plugin.rs

1use core::marker::PhantomData;
2
3use bevy_ecs::prelude::*;
4
5#[cfg(feature = "bevy_app")]
6use {crate::Parent, bevy_utils::HashSet, disqualified::ShortName};
7
8/// When enabled, runs [`check_hierarchy_component_has_valid_parent<T>`].
9///
10/// This resource is added by [`ValidParentCheckPlugin<T>`].
11/// It is enabled on debug builds and disabled in release builds by default,
12/// you can update this resource at runtime to change the default behavior.
13#[derive(Resource)]
14pub struct ReportHierarchyIssue<T> {
15    /// Whether to run [`check_hierarchy_component_has_valid_parent<T>`].
16    pub enabled: bool,
17    _comp: PhantomData<fn(T)>,
18}
19
20impl<T> ReportHierarchyIssue<T> {
21    /// Constructs a new object
22    pub fn new(enabled: bool) -> Self {
23        ReportHierarchyIssue {
24            enabled,
25            _comp: Default::default(),
26        }
27    }
28}
29
30impl<T> PartialEq for ReportHierarchyIssue<T> {
31    fn eq(&self, other: &Self) -> bool {
32        self.enabled == other.enabled
33    }
34}
35
36impl<T> Default for ReportHierarchyIssue<T> {
37    fn default() -> Self {
38        Self {
39            enabled: cfg!(debug_assertions),
40            _comp: PhantomData,
41        }
42    }
43}
44
45#[cfg(feature = "bevy_app")]
46/// System to print a warning for each [`Entity`] with a `T` component
47/// which parent hasn't a `T` component.
48///
49/// Hierarchy propagations are top-down, and limited only to entities
50/// with a specific component (such as `InheritedVisibility` and `GlobalTransform`).
51/// This means that entities with one of those component
52/// and a parent without the same component is probably a programming error.
53/// (See B0004 explanation linked in warning message)
54pub fn check_hierarchy_component_has_valid_parent<T: Component>(
55    parent_query: Query<
56        (Entity, &Parent, Option<&bevy_core::Name>),
57        (With<T>, Or<(Changed<Parent>, Added<T>)>),
58    >,
59    component_query: Query<(), With<T>>,
60    mut already_diagnosed: Local<HashSet<Entity>>,
61) {
62    for (entity, parent, name) in &parent_query {
63        let parent = parent.get();
64        if !component_query.contains(parent) && !already_diagnosed.contains(&entity) {
65            already_diagnosed.insert(entity);
66            bevy_utils::tracing::warn!(
67                "warning[B0004]: {name} with the {ty_name} component has a parent without {ty_name}.\n\
68                This will cause inconsistent behaviors! See: https://bevyengine.org/learn/errors/b0004",
69                ty_name = ShortName::of::<T>(),
70                name = name.map_or_else(|| format!("Entity {}", entity), |s| format!("The {s} entity")),
71            );
72        }
73    }
74}
75
76/// Run criteria that only allows running when [`ReportHierarchyIssue<T>`] is enabled.
77pub fn on_hierarchy_reports_enabled<T>(report: Res<ReportHierarchyIssue<T>>) -> bool
78where
79    T: Component,
80{
81    report.enabled
82}
83
84/// Print a warning for each `Entity` with a `T` component
85/// whose parent doesn't have a `T` component.
86///
87/// See [`check_hierarchy_component_has_valid_parent`] for details.
88pub struct ValidParentCheckPlugin<T: Component>(PhantomData<fn() -> T>);
89impl<T: Component> Default for ValidParentCheckPlugin<T> {
90    fn default() -> Self {
91        Self(PhantomData)
92    }
93}
94
95#[cfg(feature = "bevy_app")]
96impl<T: Component> bevy_app::Plugin for ValidParentCheckPlugin<T> {
97    fn build(&self, app: &mut bevy_app::App) {
98        app.init_resource::<ReportHierarchyIssue<T>>().add_systems(
99            bevy_app::Last,
100            check_hierarchy_component_has_valid_parent::<T>
101                .run_if(resource_equals(ReportHierarchyIssue::<T>::new(true))),
102        );
103    }
104}