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
23pub 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 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#[derive(SystemSet, PartialEq, Eq, Hash, Debug, Clone)]
60pub struct ValidateParentHasComponentSystems;
61
62fn 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 writer.write(CheckParentHasComponent::<C> {
79 entity: event.entity,
80 caller: event.caller(),
81 marker: PhantomData,
82 });
83}
84
85#[derive(Message)]
90struct CheckParentHasComponent<C: Component> {
91 entity: Entity,
93 caller: MaybeLocation,
94 marker: PhantomData<fn() -> C>,
95}
96
97fn 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 continue;
114 };
115 let parent = child_of.0;
116 let Ok(parent_name) = components.get(parent) else {
117 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}