Skip to main content

bevy_app/
hierarchy.rs

1use core::marker::PhantomData;
2
3use bevy_ecs::{
4    change_detection::MaybeLocation,
5    component::Component,
6    entity::Entity,
7    hierarchy::ChildOf,
8    intern::Interned,
9    lifecycle::Insert,
10    message::{Message, MessageReader, MessageWriter},
11    name::Name,
12    observer::On,
13    query::{With, Without},
14    schedule::{common_conditions::on_message, IntoScheduleConfigs, ScheduleLabel, SystemSet},
15    system::Query,
16};
17use bevy_platform::prelude::format;
18use bevy_utils::prelude::DebugName;
19use log::warn;
20
21use crate::{Last, Plugin};
22
23/// A plugin that verifies that [`Component`] `C` has parents that also have that component.
24pub struct ValidateParentHasComponentPlugin<C: Component> {
25    schedule: Interned<dyn ScheduleLabel>,
26    marker: PhantomData<fn() -> C>,
27}
28
29impl<C: Component> Default for ValidateParentHasComponentPlugin<C> {
30    fn default() -> Self {
31        Self::in_schedule(Last)
32    }
33}
34
35impl<C: Component> ValidateParentHasComponentPlugin<C> {
36    /// Creates an instance of this plugin that inserts systems in the provided schedule.
37    pub fn in_schedule(label: impl ScheduleLabel) -> Self {
38        Self {
39            schedule: label.intern(),
40            marker: PhantomData,
41        }
42    }
43}
44
45impl<C: Component> Plugin for ValidateParentHasComponentPlugin<C> {
46    fn build(&self, app: &mut crate::App) {
47        app.add_message::<CheckParentHasComponent<C>>()
48            .add_observer(validate_parent_has_component::<C>)
49            .add_systems(
50                self.schedule,
51                check_parent_has_component::<C>
52                    .run_if(on_message::<CheckParentHasComponent<C>>)
53                    .in_set(ValidateParentHasComponentSystems),
54            );
55    }
56}
57
58/// System set for systems added by [`ValidateParentHasComponentPlugin`].
59#[derive(SystemSet, PartialEq, Eq, Hash, Debug, Clone)]
60pub struct ValidateParentHasComponentSystems;
61
62/// An `Insert` observer that when run, will validate that the parent of a given entity contains
63/// component `C`. If the parent does not contain `C`, a warning will be logged later in the frame.
64fn validate_parent_has_component<C: Component>(
65    event: On<Insert, C>,
66    child: Query<&ChildOf>,
67    with_component: Query<(), With<C>>,
68    mut writer: MessageWriter<CheckParentHasComponent<C>>,
69) {
70    let Ok(child_of) = child.get(event.entity) else {
71        return;
72    };
73    if with_component.contains(child_of.parent()) {
74        return;
75    }
76    // This entity may be configured incorrectly, or the parent may just not have been populated
77    // yet. Send a message to check again later.
78    writer.write(CheckParentHasComponent::<C> {
79        entity: event.entity,
80        caller: event.caller(),
81        marker: PhantomData,
82    });
83}
84
85/// A message to indicate that this entity should be checked if its parent has a component.
86///
87/// While we initially check when emitting these messages, we want to do a second check later on in
88/// case the parent eventually gets populated.
89#[derive(Message)]
90struct CheckParentHasComponent<C: Component> {
91    /// The entity
92    entity: Entity,
93    caller: MaybeLocation,
94    marker: PhantomData<fn() -> C>,
95}
96
97/// System to handle "check parent" messages and log out any entities that still violate the
98/// component hierarchy.
99fn check_parent_has_component<C: Component>(
100    mut messages: MessageReader<CheckParentHasComponent<C>>,
101    children: Query<(&ChildOf, Option<&Name>), With<C>>,
102    components: Query<Option<&Name>, Without<C>>,
103) {
104    for CheckParentHasComponent {
105        entity,
106        caller,
107        marker: _,
108    } in messages.read()
109    {
110        let Ok((child_of, name)) = children.get(*entity) else {
111            // Either the entity has been despawned, no longer has `C`, or is no longer a child. In
112            // any case, we can say that this situation is no longer relevant.
113            continue;
114        };
115        let parent = child_of.0;
116        let Ok(parent_name) = components.get(parent) else {
117            // This can only fail if the parent now has the `C` component. If the parent was
118            // despawned, the child entity would also be despawned.
119            continue;
120        };
121        let debug_name = DebugName::type_name::<C>();
122        warn!(
123            "warning[B0004]: {}{name} with the {ty_name} component has a parent ({parent_name}) without {ty_name}.\n\
124            This will cause inconsistent behaviors! See: https://bevy.org/learn/errors/b0004",
125            caller.map(|c| format!("{c}: ")).unwrap_or_default(),
126            ty_name = debug_name.shortname(),
127            name = name.map_or_else(
128                || format!("Entity {entity}"),
129                |s| format!("The {s} entity")
130            ),
131            parent_name = parent_name.map_or_else(
132                || format!("{parent} entity"),
133                |s| format!("the {s} entity")
134            ),
135        );
136    }
137}