use std::marker::PhantomData; use bevy_app::{App, Last, Plugin}; use bevy_core::Name; use bevy_ecs::prelude::*; use bevy_log::warn; use bevy_utils::{get_short_name, HashSet}; use crate::Parent; /// When enabled, runs [`check_hierarchy_component_has_valid_parent`]. /// /// This resource is added by [`ValidParentCheckPlugin`]. /// It is enabled on debug builds and disabled in release builds by default, /// you can update this resource at runtime to change the default behavior. #[derive(Resource)] pub struct ReportHierarchyIssue { /// Whether to run [`check_hierarchy_component_has_valid_parent`]. pub enabled: bool, _comp: PhantomData, } impl ReportHierarchyIssue { /// Constructs a new object pub fn new(enabled: bool) -> Self { ReportHierarchyIssue { enabled, _comp: Default::default(), } } } impl PartialEq for ReportHierarchyIssue { fn eq(&self, other: &Self) -> bool { self.enabled == other.enabled } } impl Default for ReportHierarchyIssue { fn default() -> Self { Self { enabled: cfg!(debug_assertions), _comp: PhantomData, } } } /// System to print a warning for each [`Entity`] with a `T` component /// which parent hasn't a `T` component. /// /// Hierarchy propagations are top-down, and limited only to entities /// with a specific component (such as `ComputedVisibility` and `GlobalTransform`). /// This means that entities with one of those component /// and a parent without the same component is probably a programming error. /// (See B0004 explanation linked in warning message) pub fn check_hierarchy_component_has_valid_parent( parent_query: Query< (Entity, &Parent, Option<&Name>), (With, Or<(Changed, Added)>), >, component_query: Query<(), With>, mut already_diagnosed: Local>, ) { for (entity, parent, name) in &parent_query { let parent = parent.get(); if !component_query.contains(parent) && !already_diagnosed.contains(&entity) { already_diagnosed.insert(entity); warn!( "warning[B0004]: {name} with the {ty_name} component has a parent without {ty_name}.\n\ This will cause inconsistent behaviors! See https://bevyengine.org/learn/errors/#b0004", ty_name = get_short_name(std::any::type_name::()), name = name.map_or("An entity".to_owned(), |s| format!("The {s} entity")), ); } } } /// Run criteria that only allows running when [`ReportHierarchyIssue`] is enabled. pub fn on_hierarchy_reports_enabled(report: Res>) -> bool where T: Component, { report.enabled } /// Print a warning for each `Entity` with a `T` component /// whose parent doesn't have a `T` component. /// /// See [`check_hierarchy_component_has_valid_parent`] for details. pub struct ValidParentCheckPlugin(PhantomData T>); impl Default for ValidParentCheckPlugin { fn default() -> Self { Self(PhantomData) } } impl Plugin for ValidParentCheckPlugin { fn build(&self, app: &mut App) { app.init_resource::>().add_systems( Last, check_hierarchy_component_has_valid_parent:: .run_if(resource_equals(ReportHierarchyIssue::::new(true))), ); } }