From bd689360dd2139405667c7e875427947c19089c9 Mon Sep 17 00:00:00 2001 From: SpecificProtagonist Date: Tue, 17 Dec 2024 09:16:59 +0100 Subject: [PATCH 1/8] track callsites for observers & hooks ci fix new hooks in dependant crates ci --- crates/bevy_core_pipeline/src/oit/mod.rs | 7 +- crates/bevy_ecs/src/bundle.rs | 90 ++++-- crates/bevy_ecs/src/component.rs | 18 +- crates/bevy_ecs/src/entity/clone_entities.rs | 1 + crates/bevy_ecs/src/lib.rs | 8 +- .../bevy_ecs/src/observer/entity_observer.rs | 2 +- crates/bevy_ecs/src/observer/mod.rs | 81 +++++- crates/bevy_ecs/src/observer/runner.rs | 10 +- crates/bevy_ecs/src/observer/trigger_event.rs | 44 ++- crates/bevy_ecs/src/system/commands/mod.rs | 102 +++++-- crates/bevy_ecs/src/world/deferred_world.rs | 52 +++- crates/bevy_ecs/src/world/entity_ref.rs | 274 ++++++++++++++++-- crates/bevy_input_focus/src/lib.rs | 8 +- crates/bevy_input_focus/src/tab_navigation.rs | 9 +- crates/bevy_render/src/camera/camera.rs | 10 +- crates/bevy_render/src/sync_component.rs | 2 +- crates/bevy_render/src/view/visibility/mod.rs | 9 +- crates/bevy_scene/src/dynamic_scene.rs | 2 +- crates/bevy_scene/src/lib.rs | 4 +- examples/ecs/component_hooks.rs | 25 +- examples/ecs/immutable_components.rs | 15 +- 21 files changed, 655 insertions(+), 118 deletions(-) diff --git a/crates/bevy_core_pipeline/src/oit/mod.rs b/crates/bevy_core_pipeline/src/oit/mod.rs index 14e8b8d4e36e3..3ad33094bd523 100644 --- a/crates/bevy_core_pipeline/src/oit/mod.rs +++ b/crates/bevy_core_pipeline/src/oit/mod.rs @@ -71,10 +71,13 @@ impl Component for OrderIndependentTransparencySettings { type Mutability = Mutable; fn register_component_hooks(hooks: &mut ComponentHooks) { - hooks.on_add(|world, entity, _| { + hooks.on_add(|world, entity, _, caller| { if let Some(value) = world.get::(entity) { if value.layer_count > 32 { - warn!("OrderIndependentTransparencySettings layer_count set to {} might be too high.", value.layer_count); + warn!("{}OrderIndependentTransparencySettings layer_count set to {} might be too high.", + caller.map(|location|format!("{location}: ")).unwrap_or_default(), + value.layer_count + ); } } }); diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 4a49fc39f1aa5..b397f5d5d985e 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -1037,12 +1037,16 @@ impl<'w> BundleInserter<'w> { ON_REPLACE, entity, archetype_after_insert.iter_existing(), + #[cfg(feature = "track_change_detection")] + caller, ); } deferred_world.trigger_on_replace( archetype, entity, archetype_after_insert.iter_existing(), + #[cfg(feature = "track_change_detection")] + caller, ); } } @@ -1213,12 +1217,16 @@ impl<'w> BundleInserter<'w> { new_archetype, entity, archetype_after_insert.iter_added(), + #[cfg(feature = "track_change_detection")] + caller, ); if new_archetype.has_add_observer() { deferred_world.trigger_observers( ON_ADD, entity, archetype_after_insert.iter_added(), + #[cfg(feature = "track_change_detection")] + caller, ); } match insert_mode { @@ -1228,12 +1236,16 @@ impl<'w> BundleInserter<'w> { new_archetype, entity, archetype_after_insert.iter_inserted(), + #[cfg(feature = "track_change_detection")] + caller, ); if new_archetype.has_insert_observer() { deferred_world.trigger_observers( ON_INSERT, entity, archetype_after_insert.iter_inserted(), + #[cfg(feature = "track_change_detection")] + caller, ); } } @@ -1244,12 +1256,16 @@ impl<'w> BundleInserter<'w> { new_archetype, entity, archetype_after_insert.iter_added(), + #[cfg(feature = "track_change_detection")] + caller, ); if new_archetype.has_insert_observer() { deferred_world.trigger_observers( ON_INSERT, entity, archetype_after_insert.iter_added(), + #[cfg(feature = "track_change_detection")] + caller, ); } } @@ -1325,6 +1341,7 @@ impl<'w> BundleSpawner<'w> { /// # Safety /// `entity` must be allocated (but non-existent), `T` must match this [`BundleInfo`]'s type #[inline] + #[track_caller] pub unsafe fn spawn_non_existent( &mut self, entity: Entity, @@ -1372,24 +1389,32 @@ impl<'w> BundleSpawner<'w> { archetype, entity, bundle_info.iter_contributed_components(), + #[cfg(feature = "track_change_detection")] + caller, ); if archetype.has_add_observer() { deferred_world.trigger_observers( ON_ADD, entity, bundle_info.iter_contributed_components(), + #[cfg(feature = "track_change_detection")] + caller, ); } deferred_world.trigger_on_insert( archetype, entity, bundle_info.iter_contributed_components(), + #[cfg(feature = "track_change_detection")] + caller, ); if archetype.has_insert_observer() { deferred_world.trigger_observers( ON_INSERT, entity, bundle_info.iter_contributed_components(), + #[cfg(feature = "track_change_detection")] + caller, ); } }; @@ -1642,6 +1667,7 @@ fn sorted_remove(source: &mut Vec, remove: &[T]) { mod tests { use crate as bevy_ecs; use crate::{component::ComponentId, prelude::*, world::DeferredWorld}; + use core::panic::Location; #[derive(Component)] struct A; @@ -1650,19 +1676,39 @@ mod tests { #[component(on_add = a_on_add, on_insert = a_on_insert, on_replace = a_on_replace, on_remove = a_on_remove)] struct AMacroHooks; - fn a_on_add(mut world: DeferredWorld, _: Entity, _: ComponentId) { + fn a_on_add( + mut world: DeferredWorld, + _: Entity, + _: ComponentId, + _: Option<&'static Location<'static>>, + ) { world.resource_mut::().assert_order(0); } - fn a_on_insert(mut world: DeferredWorld, _: T1, _: T2) { + fn a_on_insert( + mut world: DeferredWorld, + _: T1, + _: T2, + _: Option<&'static Location<'static>>, + ) { world.resource_mut::().assert_order(1); } - fn a_on_replace(mut world: DeferredWorld, _: T1, _: T2) { + fn a_on_replace( + mut world: DeferredWorld, + _: T1, + _: T2, + _: Option<&'static Location<'static>>, + ) { world.resource_mut::().assert_order(2); } - fn a_on_remove(mut world: DeferredWorld, _: T1, _: T2) { + fn a_on_remove( + mut world: DeferredWorld, + _: T1, + _: T2, + _: Option<&'static Location<'static>>, + ) { world.resource_mut::().assert_order(3); } @@ -1695,10 +1741,10 @@ mod tests { world.init_resource::(); world .register_component_hooks::() - .on_add(|mut world, _, _| world.resource_mut::().assert_order(0)) - .on_insert(|mut world, _, _| world.resource_mut::().assert_order(1)) - .on_replace(|mut world, _, _| world.resource_mut::().assert_order(2)) - .on_remove(|mut world, _, _| world.resource_mut::().assert_order(3)); + .on_add(|mut world, _, _, _| world.resource_mut::().assert_order(0)) + .on_insert(|mut world, _, _, _| world.resource_mut::().assert_order(1)) + .on_replace(|mut world, _, _, _| world.resource_mut::().assert_order(2)) + .on_remove(|mut world, _, _, _| world.resource_mut::().assert_order(3)); let entity = world.spawn(A).id(); world.despawn(entity); @@ -1722,10 +1768,10 @@ mod tests { world.init_resource::(); world .register_component_hooks::() - .on_add(|mut world, _, _| world.resource_mut::().assert_order(0)) - .on_insert(|mut world, _, _| world.resource_mut::().assert_order(1)) - .on_replace(|mut world, _, _| world.resource_mut::().assert_order(2)) - .on_remove(|mut world, _, _| world.resource_mut::().assert_order(3)); + .on_add(|mut world, _, _, _| world.resource_mut::().assert_order(0)) + .on_insert(|mut world, _, _, _| world.resource_mut::().assert_order(1)) + .on_replace(|mut world, _, _, _| world.resource_mut::().assert_order(2)) + .on_remove(|mut world, _, _, _| world.resource_mut::().assert_order(3)); let mut entity = world.spawn_empty(); entity.insert(A); @@ -1739,8 +1785,8 @@ mod tests { let mut world = World::new(); world .register_component_hooks::() - .on_replace(|mut world, _, _| world.resource_mut::().assert_order(0)) - .on_insert(|mut world, _, _| { + .on_replace(|mut world, _, _, _| world.resource_mut::().assert_order(0)) + .on_insert(|mut world, _, _, _| { if let Some(mut r) = world.get_resource_mut::() { r.assert_order(1); } @@ -1761,22 +1807,22 @@ mod tests { world.init_resource::(); world .register_component_hooks::() - .on_add(|mut world, entity, _| { + .on_add(|mut world, entity, _, _| { world.resource_mut::().assert_order(0); world.commands().entity(entity).insert(B); }) - .on_remove(|mut world, entity, _| { + .on_remove(|mut world, entity, _, _| { world.resource_mut::().assert_order(2); world.commands().entity(entity).remove::(); }); world .register_component_hooks::() - .on_add(|mut world, entity, _| { + .on_add(|mut world, entity, _, _| { world.resource_mut::().assert_order(1); world.commands().entity(entity).remove::(); }) - .on_remove(|mut world, _, _| { + .on_remove(|mut world, _, _, _| { world.resource_mut::().assert_order(3); }); @@ -1793,27 +1839,27 @@ mod tests { world.init_resource::(); world .register_component_hooks::() - .on_add(|mut world, entity, _| { + .on_add(|mut world, entity, _, _| { world.resource_mut::().assert_order(0); world.commands().entity(entity).insert(B).insert(C); }); world .register_component_hooks::() - .on_add(|mut world, entity, _| { + .on_add(|mut world, entity, _, _| { world.resource_mut::().assert_order(1); world.commands().entity(entity).insert(D); }); world .register_component_hooks::() - .on_add(|mut world, _, _| { + .on_add(|mut world, _, _, _| { world.resource_mut::().assert_order(3); }); world .register_component_hooks::() - .on_add(|mut world, _, _| { + .on_add(|mut world, _, _, _| { world.resource_mut::().assert_order(2); }); diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index e6eaafb900f5c..4ea2c3d7da1b3 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -17,8 +17,6 @@ use bevy_ptr::{OwningPtr, UnsafeCellDeref}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; use bevy_utils::{HashMap, HashSet, TypeIdMap}; -#[cfg(feature = "track_change_detection")] -use core::panic::Location; use core::{ alloc::Layout, any::{Any, TypeId}, @@ -26,6 +24,7 @@ use core::{ fmt::Debug, marker::PhantomData, mem::needs_drop, + panic::Location, }; use disqualified::ShortName; use thiserror::Error; @@ -300,6 +299,7 @@ pub use bevy_ecs_macros::require; /// # use bevy_ecs::world::DeferredWorld; /// # use bevy_ecs::entity::Entity; /// # use bevy_ecs::component::ComponentId; +/// # use core::panic::Location; /// # /// #[derive(Component)] /// #[component(on_add = my_on_add_hook)] @@ -311,12 +311,12 @@ pub use bevy_ecs_macros::require; /// // #[component(on_replace = my_on_replace_hook, on_remove = my_on_remove_hook)] /// struct ComponentA; /// -/// fn my_on_add_hook(world: DeferredWorld, entity: Entity, id: ComponentId) { +/// fn my_on_add_hook(world: DeferredWorld, entity: Entity, id: ComponentId, caller: Option<&'static Location<'static>>) { /// // ... /// } /// /// // You can also omit writing some types using generics. -/// fn my_on_insert_hook(world: DeferredWorld, _: T1, _: T2) { +/// fn my_on_insert_hook(world: DeferredWorld, _: T1, _: T2, caller: Option<&'static Location<'static>>) { /// // ... /// } /// ``` @@ -493,8 +493,10 @@ pub enum StorageType { SparseSet, } -/// The type used for [`Component`] lifecycle hooks such as `on_add`, `on_insert` or `on_remove` -pub type ComponentHook = for<'w> fn(DeferredWorld<'w>, Entity, ComponentId); +/// The type used for [`Component`] lifecycle hooks such as `on_add`, `on_insert` or `on_remove`. +/// The caller location is `Some` if the `track_change_detection` feature is enabled. +pub type ComponentHook = + for<'w> fn(DeferredWorld<'w>, Entity, ComponentId, Option<&'static Location<'static>>); /// [`World`]-mutating functions that run as part of lifecycle events of a [`Component`]. /// @@ -531,12 +533,12 @@ pub type ComponentHook = for<'w> fn(DeferredWorld<'w>, Entity, ComponentId); /// let mut tracked_component_query = world.query::<&MyTrackedComponent>(); /// assert!(tracked_component_query.iter(&world).next().is_none()); /// -/// world.register_component_hooks::().on_add(|mut world, entity, _component_id| { +/// world.register_component_hooks::().on_add(|mut world, entity, _component_id, _caller| { /// let mut tracked_entities = world.resource_mut::(); /// tracked_entities.0.insert(entity); /// }); /// -/// world.register_component_hooks::().on_remove(|mut world, entity, _component_id| { +/// world.register_component_hooks::().on_remove(|mut world, entity, _component_id, _caller| { /// let mut tracked_entities = world.resource_mut::(); /// tracked_entities.0.remove(&entity); /// }); diff --git a/crates/bevy_ecs/src/entity/clone_entities.rs b/crates/bevy_ecs/src/entity/clone_entities.rs index 13029aa8a7423..86c8befbf036b 100644 --- a/crates/bevy_ecs/src/entity/clone_entities.rs +++ b/crates/bevy_ecs/src/entity/clone_entities.rs @@ -23,6 +23,7 @@ pub struct EntityCloner { impl EntityCloner { /// Clones and inserts components from the `source` entity into `target` entity using the stored configuration. + #[track_caller] pub fn clone_entity(&mut self, world: &mut World) { let source_entity = world .get_entity(self.source) diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index ccc2aa702f43e..3d8f914e24ffb 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -1983,8 +1983,8 @@ mod tests { world.insert_resource(I(0)); world .register_component_hooks::() - .on_add(|mut world, _, _| world.resource_mut::().0 += 1) - .on_insert(|mut world, _, _| world.resource_mut::().0 += 1); + .on_add(|mut world, _, _, _| world.resource_mut::().0 += 1) + .on_insert(|mut world, _, _, _| world.resource_mut::().0 += 1); // Spawn entity and ensure Y was added assert!(world.spawn(X).contains::()); @@ -2013,8 +2013,8 @@ mod tests { world.insert_resource(I(0)); world .register_component_hooks::() - .on_add(|mut world, _, _| world.resource_mut::().0 += 1) - .on_insert(|mut world, _, _| world.resource_mut::().0 += 1); + .on_add(|mut world, _, _, _| world.resource_mut::().0 += 1) + .on_insert(|mut world, _, _, _| world.resource_mut::().0 += 1); // Spawn entity and ensure Y was added assert!(world.spawn_empty().insert(X).contains::()); diff --git a/crates/bevy_ecs/src/observer/entity_observer.rs b/crates/bevy_ecs/src/observer/entity_observer.rs index 6fd39a4b42546..9ef5a2a5d3937 100644 --- a/crates/bevy_ecs/src/observer/entity_observer.rs +++ b/crates/bevy_ecs/src/observer/entity_observer.rs @@ -14,7 +14,7 @@ impl Component for ObservedBy { type Mutability = Mutable; fn register_component_hooks(hooks: &mut ComponentHooks) { - hooks.on_remove(|mut world, entity, _| { + hooks.on_remove(|mut world, entity, _, _| { let observed_by = { let mut component = world.get_mut::(entity).unwrap(); core::mem::take(&mut component.0) diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 0cbb1b80b263e..b3d5b76611b0d 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -19,6 +19,8 @@ use crate::{ }; use bevy_ptr::Ptr; use bevy_utils::HashMap; +#[cfg(feature = "track_change_detection")] +use core::panic::Location; use core::{ fmt::Debug, marker::PhantomData, @@ -221,6 +223,10 @@ pub struct ObserverTrigger { components: SmallVec<[ComponentId; 2]>, /// The entity the trigger targeted. pub target: Entity, + + /// The location of the source code that triggered the obserer. + #[cfg(feature = "track_change_detection")] + pub caller: &'static Location<'static>, } impl ObserverTrigger { @@ -294,6 +300,7 @@ impl Observers { components: impl Iterator + Clone, data: &mut T, propagate: &mut bool, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, ) { // SAFETY: You cannot get a mutable reference to `observers` from `DeferredWorld` let (mut world, observers) = unsafe { @@ -318,6 +325,8 @@ impl Observers { event_type, components: components.clone().collect(), target, + #[cfg(feature = "track_change_detection")] + caller, }, data.into(), propagate, @@ -430,16 +439,30 @@ impl World { /// While event types commonly implement [`Copy`], /// those that don't will be consumed and will no longer be accessible. /// If you need to use the event after triggering it, use [`World::trigger_ref`] instead. + #[track_caller] pub fn trigger(&mut self, event: impl Event) { - TriggerEvent { event, targets: () }.trigger(self); + TriggerEvent { + event, + targets: (), + #[cfg(feature = "track_change_detection")] + caller: Location::caller(), + } + .trigger(self); } /// Triggers the given [`Event`] as a mutable reference, which will run any [`Observer`]s watching for it. /// /// Compared to [`World::trigger`], this method is most useful when it's necessary to check /// or use the event after it has been modified by observers. + #[track_caller] pub fn trigger_ref(&mut self, event: &mut impl Event) { - TriggerEvent { event, targets: () }.trigger_ref(self); + TriggerEvent { + event, + targets: (), + #[cfg(feature = "track_change_detection")] + caller: Location::caller(), + } + .trigger_ref(self); } /// Triggers the given [`Event`] for the given `targets`, which will run any [`Observer`]s watching for it. @@ -447,8 +470,15 @@ impl World { /// While event types commonly implement [`Copy`], /// those that don't will be consumed and will no longer be accessible. /// If you need to use the event after triggering it, use [`World::trigger_targets_ref`] instead. + #[track_caller] pub fn trigger_targets(&mut self, event: impl Event, targets: impl TriggerTargets) { - TriggerEvent { event, targets }.trigger(self); + TriggerEvent { + event, + targets, + #[cfg(feature = "track_change_detection")] + caller: Location::caller(), + } + .trigger(self); } /// Triggers the given [`Event`] as a mutable reference for the given `targets`, @@ -456,8 +486,15 @@ impl World { /// /// Compared to [`World::trigger_targets`], this method is most useful when it's necessary to check /// or use the event after it has been modified by observers. + #[track_caller] pub fn trigger_targets_ref(&mut self, event: &mut impl Event, targets: impl TriggerTargets) { - TriggerEvent { event, targets }.trigger_ref(self); + TriggerEvent { + event, + targets, + #[cfg(feature = "track_change_detection")] + caller: Location::caller(), + } + .trigger_ref(self); } /// Register an observer to the cache, called when an observer is created @@ -582,6 +619,8 @@ impl World { #[cfg(test)] mod tests { use alloc::vec; + #[cfg(feature = "track_change_detection")] + use core::panic::Location; use bevy_ptr::OwningPtr; use bevy_utils::HashMap; @@ -1338,6 +1377,40 @@ mod tests { assert!(world.get_resource::().is_some()); } + #[test] + #[cfg(feature = "track_change_detection")] + #[track_caller] + fn observer_caller_location_event() { + #[derive(Event)] + struct EventA; + + let caller = Location::caller(); + let mut world = World::new(); + world.add_observer(move |trigger: Trigger| { + assert_eq!(trigger.trigger.caller, caller); + }); + world.trigger(EventA); + } + + #[test] + #[cfg(feature = "track_change_detection")] + #[track_caller] + fn observer_caller_location_command_archetype_move() { + #[derive(Component)] + struct Component; + + let caller = Location::caller(); + let mut world = World::new(); + world.add_observer(move |trigger: Trigger| { + assert_eq!(trigger.trigger.caller, caller); + }); + world.add_observer(move |trigger: Trigger| { + assert_eq!(trigger.trigger.caller, caller); + }); + world.commands().spawn(Component).clear(); + world.flush_commands(); + } + #[test] fn observer_triggered_components() { #[derive(Resource, Default)] diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index f7ee95972d435..2d75b64465b69 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -1,4 +1,5 @@ use core::any::Any; +use core::panic::Location; use crate::{ component::{ComponentHook, ComponentHooks, ComponentId, Mutable, StorageType}, @@ -65,12 +66,12 @@ impl Component for ObserverState { type Mutability = Mutable; fn register_component_hooks(hooks: &mut ComponentHooks) { - hooks.on_add(|mut world, entity, _| { + hooks.on_add(|mut world, entity, _, _| { world.commands().queue(move |world: &mut World| { world.register_observer(entity); }); }); - hooks.on_remove(|mut world, entity, _| { + hooks.on_remove(|mut world, entity, _, _| { let descriptor = core::mem::take( &mut world .entity_mut(entity) @@ -317,12 +318,12 @@ impl Component for Observer { const STORAGE_TYPE: StorageType = StorageType::SparseSet; type Mutability = Mutable; fn register_component_hooks(hooks: &mut ComponentHooks) { - hooks.on_add(|world, entity, _id| { + hooks.on_add(|world, entity, id, caller| { let Some(observe) = world.get::(entity) else { return; }; let hook = observe.hook_on_add; - hook(world, entity, _id); + hook(world, entity, id, caller); }); } } @@ -395,6 +396,7 @@ fn hook_on_add>( mut world: DeferredWorld<'_>, entity: Entity, _: ComponentId, + _: Option<&'static Location<'static>>, ) { world.commands().queue(move |world: &mut World| { let event_type = world.register_component::(); diff --git a/crates/bevy_ecs/src/observer/trigger_event.rs b/crates/bevy_ecs/src/observer/trigger_event.rs index 5221aff070e2e..96eafa6746ec2 100644 --- a/crates/bevy_ecs/src/observer/trigger_event.rs +++ b/crates/bevy_ecs/src/observer/trigger_event.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "track_change_detection")] +use core::panic::Location; + use crate::{ component::ComponentId, entity::Entity, @@ -12,19 +15,37 @@ pub struct TriggerEvent { /// The targets to trigger the event for. pub targets: Targets, + + /// The source code that emitted this command. + #[cfg(feature = "track_change_detection")] + pub caller: &'static Location<'static>, } impl TriggerEvent { pub(super) fn trigger(mut self, world: &mut World) { let event_type = world.register_component::(); - trigger_event(world, event_type, &mut self.event, self.targets); + trigger_event( + world, + event_type, + &mut self.event, + self.targets, + #[cfg(feature = "track_change_detection")] + self.caller, + ); } } impl TriggerEvent<&mut E, Targets> { pub(super) fn trigger_ref(self, world: &mut World) { let event_type = world.register_component::(); - trigger_event(world, event_type, self.event, self.targets); + trigger_event( + world, + event_type, + self.event, + self.targets, + #[cfg(feature = "track_change_detection")] + self.caller, + ); } } @@ -41,17 +62,22 @@ pub struct EmitDynamicTrigger { event_type: ComponentId, event_data: T, targets: Targets, + #[cfg(feature = "track_change_detection")] + caller: &'static Location<'static>, } impl EmitDynamicTrigger { /// Sets the event type of the resulting trigger, used for dynamic triggers /// # Safety /// Caller must ensure that the component associated with `event_type` is accessible as E + #[track_caller] pub unsafe fn new_with_id(event_type: ComponentId, event_data: E, targets: Targets) -> Self { Self { event_type, event_data, targets, + #[cfg(feature = "track_change_detection")] + caller: Location::caller(), } } } @@ -60,7 +86,14 @@ impl Command for EmitDynamicTrigger { fn apply(mut self, world: &mut World) { - trigger_event(world, self.event_type, &mut self.event_data, self.targets); + trigger_event( + world, + self.event_type, + &mut self.event_data, + self.targets, + #[cfg(feature = "track_change_detection")] + self.caller, + ); } } @@ -70,6 +103,7 @@ fn trigger_event( event_type: ComponentId, event_data: &mut E, targets: Targets, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, ) { let mut world = DeferredWorld::from(world); if targets.entities().is_empty() { @@ -81,6 +115,8 @@ fn trigger_event( targets.components(), event_data, false, + #[cfg(feature = "track_change_detection")] + caller, ); }; } else { @@ -93,6 +129,8 @@ fn trigger_event( targets.components(), event_data, E::AUTO_PROPAGATE, + #[cfg(feature = "track_change_detection")] + caller, ); }; } diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 03f69acd61ef7..684b138f0cbee 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -958,20 +958,32 @@ impl<'w, 's> Commands<'w, 's> { /// isn't scoped to specific targets. /// /// [`Trigger`]: crate::observer::Trigger + #[track_caller] pub fn trigger(&mut self, event: impl Event) { - self.queue(TriggerEvent { event, targets: () }); + self.queue(TriggerEvent { + event, + targets: (), + #[cfg(feature = "track_change_detection")] + caller: Location::caller(), + }); } /// Sends a [`Trigger`] for the given targets. This will run any [`Observer`] of the `event` that /// watches those targets. /// /// [`Trigger`]: crate::observer::Trigger + #[track_caller] pub fn trigger_targets( &mut self, event: impl Event, targets: impl TriggerTargets + Send + Sync + 'static, ) { - self.queue(TriggerEvent { event, targets }); + self.queue(TriggerEvent { + event, + targets, + #[cfg(feature = "track_change_detection")] + caller: Location::caller(), + }); } /// Spawns an [`Observer`] and returns the [`EntityCommands`] associated @@ -1504,6 +1516,7 @@ impl<'a> EntityCommands<'a> { /// } /// # bevy_ecs::system::assert_is_system(add_health_system); /// ``` + #[track_caller] pub fn try_insert_if_new_and(&mut self, bundle: impl Bundle, condition: F) -> &mut Self where F: FnOnce() -> bool, @@ -1524,6 +1537,7 @@ impl<'a> EntityCommands<'a> { /// # Note /// /// Unlike [`Self::insert_if_new`], this will not panic if the associated entity does not exist. + #[track_caller] pub fn try_insert_if_new(&mut self, bundle: impl Bundle) -> &mut Self { self.queue(try_insert(bundle, InsertMode::Keep)) } @@ -1563,11 +1577,12 @@ impl<'a> EntityCommands<'a> { /// } /// # bevy_ecs::system::assert_is_system(remove_combat_stats_system); /// ``` + #[track_caller] pub fn remove(&mut self) -> &mut Self where T: Bundle, { - self.queue(remove::) + self.queue(remove::()) } /// Removes all components in the [`Bundle`] components and remove all required components for each component in the [`Bundle`] from entity. @@ -1594,16 +1609,19 @@ impl<'a> EntityCommands<'a> { /// } /// # bevy_ecs::system::assert_is_system(remove_with_requires_system); /// ``` + #[track_caller] pub fn remove_with_requires(&mut self) -> &mut Self { - self.queue(remove_with_requires::) + self.queue(remove_with_requires::()) } /// Removes a component from the entity. + #[track_caller] pub fn remove_by_id(&mut self, component_id: ComponentId) -> &mut Self { self.queue(remove_by_id(component_id)) } /// Removes all components associated with the entity. + #[track_caller] pub fn clear(&mut self) -> &mut Self { self.queue(clear()) } @@ -1710,7 +1728,7 @@ impl<'a> EntityCommands<'a> { where T: Bundle, { - self.queue(retain::) + self.queue(retain::()) } /// Logs the components of the entity at the info level. @@ -2066,7 +2084,6 @@ where I: IntoIterator + Send + Sync + 'static, B: Bundle, { - #[cfg(feature = "track_change_detection")] let caller = Location::caller(); move |world: &mut World| { if let Err(invalid_entities) = world.insert_or_spawn_batch_with_caller( @@ -2075,7 +2092,7 @@ where caller, ) { error!( - "Failed to 'insert or spawn' bundle of type {} into the following invalid entities: {:?}", + "{caller}: Failed to 'insert or spawn' bundle of type {} into the following invalid entities: {:?}", core::any::type_name::(), invalid_entities ); @@ -2262,18 +2279,26 @@ fn try_insert(bundle: impl Bundle, mode: InsertMode) -> impl EntityCommand { /// /// - The returned `EntityCommand` must be queued for the world where `component_id` was created. /// - `T` must be the type represented by `component_id`. +#[track_caller] unsafe fn insert_by_id( component_id: ComponentId, value: T, on_none_entity: impl FnOnce(&mut World, Entity) + Send + 'static, ) -> impl EntityCommand { + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); move |entity: Entity, world: &mut World| { if let Ok(mut entity) = world.get_entity_mut(entity) { // SAFETY: // - `component_id` safety is ensured by the caller // - `ptr` is valid within the `make` block; OwningPtr::make(value, |ptr| unsafe { - entity.insert_by_id(component_id, ptr); + entity.insert_by_id_with_caller( + component_id, + ptr, + #[cfg(feature = "track_change_detection")] + caller, + ); }); } else { on_none_entity(world, entity); @@ -2285,36 +2310,65 @@ unsafe fn insert_by_id( /// /// For a [`Bundle`] type `T`, this will remove any components in the bundle. /// Any components in the bundle that aren't found on the entity will be ignored. -fn remove(entity: Entity, world: &mut World) { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.remove::(); +#[track_caller] +fn remove() -> impl EntityCommand { + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); + move |entity: Entity, world: &mut World| { + if let Ok(mut entity) = world.get_entity_mut(entity) { + entity.remove_with_caller::( + #[cfg(feature = "track_change_detection")] + caller, + ); + } } } /// An [`EntityCommand`] that removes components with a provided [`ComponentId`] from an entity. -/// # Panics /// +/// # Panics /// Panics if the provided [`ComponentId`] does not exist in the [`World`]. +#[track_caller] fn remove_by_id(component_id: ComponentId) -> impl EntityCommand { + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); move |entity: Entity, world: &mut World| { if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.remove_by_id(component_id); + entity.remove_by_id_with_caller( + component_id, + #[cfg(feature = "track_change_detection")] + caller, + ); } } } /// An [`EntityCommand`] that remove all components in the bundle and remove all required components for each component in the bundle. -fn remove_with_requires(entity: Entity, world: &mut World) { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.remove_with_requires::(); +#[track_caller] +fn remove_with_requires() -> impl EntityCommand { + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); + move |entity: Entity, world: &mut World| { + if let Ok(mut entity) = world.get_entity_mut(entity) { + entity.remove_with_requires_with_caller::( + #[cfg(feature = "track_change_detection")] + caller, + ); + } } } /// An [`EntityCommand`] that removes all components associated with a provided entity. +#[track_caller] fn clear() -> impl EntityCommand { + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); move |entity: Entity, world: &mut World| { if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.clear(); + entity.clear_with_caller( + #[cfg(feature = "track_change_detection")] + caller, + ); } } } @@ -2323,9 +2377,17 @@ fn clear() -> impl EntityCommand { /// /// For a [`Bundle`] type `T`, this will remove all components except those in the bundle. /// Any components in the bundle that aren't found on the entity will be ignored. -fn retain(entity: Entity, world: &mut World) { - if let Ok(mut entity_mut) = world.get_entity_mut(entity) { - entity_mut.retain::(); +#[track_caller] +fn retain() -> impl EntityCommand { + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); + move |entity: Entity, world: &mut World| { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.retain_with_caller::( + #[cfg(feature = "track_change_detection")] + caller, + ); + } } } diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 3002a11f58667..1257ca2b7519f 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -1,4 +1,6 @@ use core::ops::Deref; +#[cfg(feature = "track_change_detection")] +use core::panic::Location; use crate::{ archetype::Archetype, @@ -422,13 +424,22 @@ impl<'w> DeferredWorld<'w> { archetype: &Archetype, entity: Entity, targets: impl Iterator, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, ) { if archetype.has_add_hook() { for component_id in targets { // SAFETY: Caller ensures that these components exist let hooks = unsafe { self.components().get_info_unchecked(component_id) }.hooks(); if let Some(hook) = hooks.on_add { - hook(DeferredWorld { world: self.world }, entity, component_id); + hook( + DeferredWorld { world: self.world }, + entity, + component_id, + #[cfg(feature = "track_change_detection")] + Some(caller), + #[cfg(not(feature = "track_change_detection"))] + None, + ); } } } @@ -444,13 +455,22 @@ impl<'w> DeferredWorld<'w> { archetype: &Archetype, entity: Entity, targets: impl Iterator, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, ) { if archetype.has_insert_hook() { for component_id in targets { // SAFETY: Caller ensures that these components exist let hooks = unsafe { self.components().get_info_unchecked(component_id) }.hooks(); if let Some(hook) = hooks.on_insert { - hook(DeferredWorld { world: self.world }, entity, component_id); + hook( + DeferredWorld { world: self.world }, + entity, + component_id, + #[cfg(feature = "track_change_detection")] + Some(caller), + #[cfg(not(feature = "track_change_detection"))] + None, + ); } } } @@ -466,13 +486,22 @@ impl<'w> DeferredWorld<'w> { archetype: &Archetype, entity: Entity, targets: impl Iterator, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, ) { if archetype.has_replace_hook() { for component_id in targets { // SAFETY: Caller ensures that these components exist let hooks = unsafe { self.components().get_info_unchecked(component_id) }.hooks(); if let Some(hook) = hooks.on_replace { - hook(DeferredWorld { world: self.world }, entity, component_id); + hook( + DeferredWorld { world: self.world }, + entity, + component_id, + #[cfg(feature = "track_change_detection")] + Some(caller), + #[cfg(not(feature = "track_change_detection"))] + None, + ); } } } @@ -488,13 +517,22 @@ impl<'w> DeferredWorld<'w> { archetype: &Archetype, entity: Entity, targets: impl Iterator, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, ) { if archetype.has_remove_hook() { for component_id in targets { // SAFETY: Caller ensures that these components exist let hooks = unsafe { self.components().get_info_unchecked(component_id) }.hooks(); if let Some(hook) = hooks.on_remove { - hook(DeferredWorld { world: self.world }, entity, component_id); + hook( + DeferredWorld { world: self.world }, + entity, + component_id, + #[cfg(feature = "track_change_detection")] + Some(caller), + #[cfg(not(feature = "track_change_detection"))] + None, + ); } } } @@ -510,6 +548,7 @@ impl<'w> DeferredWorld<'w> { event: ComponentId, target: Entity, components: impl Iterator + Clone, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, ) { Observers::invoke::<_>( self.reborrow(), @@ -518,6 +557,8 @@ impl<'w> DeferredWorld<'w> { components, &mut (), &mut false, + #[cfg(feature = "track_change_detection")] + caller, ); } @@ -533,6 +574,7 @@ impl<'w> DeferredWorld<'w> { components: &[ComponentId], data: &mut E, mut propagate: bool, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, ) where T: Traversal, { @@ -544,6 +586,8 @@ impl<'w> DeferredWorld<'w> { components.iter().copied(), data, &mut propagate, + #[cfg(feature = "track_change_detection")] + caller, ); if !propagate { break; diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index c02bbd0b34671..cb606103906ba 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -1362,7 +1362,14 @@ impl<'w> EntityWorldMut<'w> { self.location = // SAFETY: location matches current entity. `T` matches `bundle_info` unsafe { - bundle_inserter.insert(self.entity, self.location, bundle, mode, #[cfg(feature = "track_change_detection")] caller) + bundle_inserter.insert( + self.entity, + self.location, + bundle, + mode, + #[cfg(feature = "track_change_detection")] + caller + ) }; self.world.flush(); self.update_location(); @@ -1388,6 +1395,23 @@ impl<'w> EntityWorldMut<'w> { &mut self, component_id: ComponentId, component: OwningPtr<'_>, + ) -> &mut Self { + self.insert_by_id_with_caller( + component_id, + component, + #[cfg(feature = "track_change_detection")] + Location::caller(), + ) + } + + /// # Safety + /// See [`EntityWorldMut::insert_by_id`] + #[inline] + pub(crate) unsafe fn insert_by_id_with_caller( + &mut self, + component_id: ComponentId, + component: OwningPtr<'_>, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, ) -> &mut Self { self.assert_not_despawned(); let change_tick = self.world.change_tick(); @@ -1410,6 +1434,8 @@ impl<'w> EntityWorldMut<'w> { self.location, Some(component).into_iter(), Some(storage_type).iter().cloned(), + #[cfg(feature = "track_change_detection")] + caller, ); self.world.flush(); self.update_location(); @@ -1459,6 +1485,8 @@ impl<'w> EntityWorldMut<'w> { self.location, iter_components, (*storage_types).iter().cloned(), + #[cfg(feature = "track_change_detection")] + Location::caller(), ); *self.world.bundles.get_storages_unchecked(bundle_id) = core::mem::take(&mut storage_types); self.world.flush(); @@ -1476,6 +1504,7 @@ impl<'w> EntityWorldMut<'w> { /// If the entity has been despawned while this `EntityWorldMut` is still alive. // TODO: BundleRemover? #[must_use] + #[track_caller] pub fn take(&mut self) -> Option { self.assert_not_despawned(); let world = &mut self.world; @@ -1521,6 +1550,8 @@ impl<'w> EntityWorldMut<'w> { old_archetype, entity, bundle_info, + #[cfg(feature = "track_change_detection")] + Location::caller(), ); } @@ -1660,7 +1691,11 @@ impl<'w> EntityWorldMut<'w> { /// # Safety /// - A `BundleInfo` with the corresponding `BundleId` must have been initialized. #[allow(clippy::too_many_arguments)] - unsafe fn remove_bundle(&mut self, bundle: BundleId) -> EntityLocation { + unsafe fn remove_bundle( + &mut self, + bundle: BundleId, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + ) -> EntityLocation { let entity = self.entity; let world = &mut self.world; let location = self.location; @@ -1703,6 +1738,8 @@ impl<'w> EntityWorldMut<'w> { old_archetype, entity, bundle_info, + #[cfg(feature = "track_change_detection")] + caller, ); } @@ -1749,14 +1786,32 @@ impl<'w> EntityWorldMut<'w> { /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. // TODO: BundleRemover? + #[track_caller] pub fn remove(&mut self) -> &mut Self { + self.remove_with_caller::( + #[cfg(feature = "track_change_detection")] + Location::caller(), + ) + } + + #[inline] + pub(crate) fn remove_with_caller( + &mut self, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + ) -> &mut Self { self.assert_not_despawned(); let storages = &mut self.world.storages; let components = &mut self.world.components; let bundle_info = self.world.bundles.register_info::(components, storages); // SAFETY: the `BundleInfo` is initialized above - self.location = unsafe { self.remove_bundle(bundle_info) }; + self.location = unsafe { + self.remove_bundle( + bundle_info, + #[cfg(feature = "track_change_detection")] + caller, + ) + }; self.world.flush(); self.update_location(); self @@ -1767,7 +1822,18 @@ impl<'w> EntityWorldMut<'w> { /// # Panics /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. + #[track_caller] pub fn remove_with_requires(&mut self) -> &mut Self { + self.remove_with_requires_with_caller::( + #[cfg(feature = "track_change_detection")] + Location::caller(), + ) + } + + pub(crate) fn remove_with_requires_with_caller( + &mut self, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + ) -> &mut Self { self.assert_not_despawned(); let storages = &mut self.world.storages; let components = &mut self.world.components; @@ -1776,7 +1842,13 @@ impl<'w> EntityWorldMut<'w> { let bundle_id = bundles.register_contributed_bundle_info::(components, storages); // SAFETY: the dynamic `BundleInfo` is initialized above - self.location = unsafe { self.remove_bundle(bundle_id) }; + self.location = unsafe { + self.remove_bundle( + bundle_id, + #[cfg(feature = "track_change_detection")] + caller, + ) + }; self.world.flush(); self.update_location(); self @@ -1789,7 +1861,19 @@ impl<'w> EntityWorldMut<'w> { /// # Panics /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. + #[track_caller] pub fn retain(&mut self) -> &mut Self { + self.retain_with_caller::( + #[cfg(feature = "track_change_detection")] + Location::caller(), + ) + } + + #[inline] + pub(crate) fn retain_with_caller( + &mut self, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + ) -> &mut Self { self.assert_not_despawned(); let archetypes = &mut self.world.archetypes; let storages = &mut self.world.storages; @@ -1809,7 +1893,13 @@ impl<'w> EntityWorldMut<'w> { let remove_bundle = self.world.bundles.init_dynamic_info(components, to_remove); // SAFETY: the `BundleInfo` for the components to remove is initialized above - self.location = unsafe { self.remove_bundle(remove_bundle) }; + self.location = unsafe { + self.remove_bundle( + remove_bundle, + #[cfg(feature = "track_change_detection")] + caller, + ) + }; self.world.flush(); self.update_location(); self @@ -1823,7 +1913,21 @@ impl<'w> EntityWorldMut<'w> { /// /// Panics if the provided [`ComponentId`] does not exist in the [`World`] or if the /// entity has been despawned while this `EntityWorldMut` is still alive. + #[track_caller] pub fn remove_by_id(&mut self, component_id: ComponentId) -> &mut Self { + self.remove_by_id_with_caller( + component_id, + #[cfg(feature = "track_change_detection")] + Location::caller(), + ) + } + + #[inline] + pub(crate) fn remove_by_id_with_caller( + &mut self, + component_id: ComponentId, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + ) -> &mut Self { self.assert_not_despawned(); let components = &mut self.world.components; @@ -1833,7 +1937,13 @@ impl<'w> EntityWorldMut<'w> { .init_component_info(components, component_id); // SAFETY: the `BundleInfo` for this `component_id` is initialized above - self.location = unsafe { self.remove_bundle(bundle_id) }; + self.location = unsafe { + self.remove_bundle( + bundle_id, + #[cfg(feature = "track_change_detection")] + caller, + ) + }; self.world.flush(); self.update_location(); self @@ -1847,6 +1957,7 @@ impl<'w> EntityWorldMut<'w> { /// /// Panics if any of the provided [`ComponentId`]s do not exist in the [`World`] or if the /// entity has been despawned while this `EntityWorldMut` is still alive. + #[track_caller] pub fn remove_by_ids(&mut self, component_ids: &[ComponentId]) -> &mut Self { self.assert_not_despawned(); let components = &mut self.world.components; @@ -1857,7 +1968,13 @@ impl<'w> EntityWorldMut<'w> { .init_dynamic_info(components, component_ids); // SAFETY: the `BundleInfo` for this `bundle_id` is initialized above - unsafe { self.remove_bundle(bundle_id) }; + unsafe { + self.remove_bundle( + bundle_id, + #[cfg(feature = "track_change_detection")] + Location::caller(), + ) + }; self.world.flush(); self.update_location(); @@ -1869,7 +1986,20 @@ impl<'w> EntityWorldMut<'w> { /// # Panics /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. + #[track_caller] pub fn clear(&mut self) -> &mut Self { + self.clear_with_caller( + #[cfg(feature = "track_change_detection")] + Location::caller(), + ) + } + + #[inline] + pub(crate) fn clear_with_caller( + &mut self, + + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + ) -> &mut Self { self.assert_not_despawned(); let component_ids: Vec = self.archetype().components().collect(); let components = &mut self.world.components; @@ -1880,7 +2010,13 @@ impl<'w> EntityWorldMut<'w> { .init_dynamic_info(components, component_ids.as_slice()); // SAFETY: the `BundleInfo` for this `component_id` is initialized above - self.location = unsafe { self.remove_bundle(bundle_id) }; + self.location = unsafe { + self.remove_bundle( + bundle_id, + #[cfg(feature = "track_change_detection")] + caller, + ) + }; self.world.flush(); self.update_location(); self @@ -1919,13 +2055,37 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: All components in the archetype exist in world unsafe { if archetype.has_replace_observer() { - deferred_world.trigger_observers(ON_REPLACE, self.entity, archetype.components()); + deferred_world.trigger_observers( + ON_REPLACE, + self.entity, + archetype.components(), + #[cfg(feature = "track_change_detection")] + Location::caller(), + ); } - deferred_world.trigger_on_replace(archetype, self.entity, archetype.components()); + deferred_world.trigger_on_replace( + archetype, + self.entity, + archetype.components(), + #[cfg(feature = "track_change_detection")] + Location::caller(), + ); if archetype.has_remove_observer() { - deferred_world.trigger_observers(ON_REMOVE, self.entity, archetype.components()); + deferred_world.trigger_observers( + ON_REMOVE, + self.entity, + archetype.components(), + #[cfg(feature = "track_change_detection")] + Location::caller(), + ); } - deferred_world.trigger_on_remove(archetype, self.entity, archetype.components()); + deferred_world.trigger_on_remove( + archetype, + self.entity, + archetype.components(), + #[cfg(feature = "track_change_detection")] + Location::caller(), + ); } for component_id in archetype.components() { @@ -2346,19 +2506,40 @@ unsafe fn trigger_on_replace_and_on_remove_hooks_and_observers( archetype: &Archetype, entity: Entity, bundle_info: &BundleInfo, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, ) { if archetype.has_replace_observer() { deferred_world.trigger_observers( ON_REPLACE, entity, bundle_info.iter_explicit_components(), + #[cfg(feature = "track_change_detection")] + caller, ); } - deferred_world.trigger_on_replace(archetype, entity, bundle_info.iter_explicit_components()); + deferred_world.trigger_on_replace( + archetype, + entity, + bundle_info.iter_explicit_components(), + #[cfg(feature = "track_change_detection")] + caller, + ); if archetype.has_remove_observer() { - deferred_world.trigger_observers(ON_REMOVE, entity, bundle_info.iter_explicit_components()); + deferred_world.trigger_observers( + ON_REMOVE, + entity, + bundle_info.iter_explicit_components(), + #[cfg(feature = "track_change_detection")] + caller, + ); } - deferred_world.trigger_on_remove(archetype, entity, bundle_info.iter_explicit_components()); + deferred_world.trigger_on_remove( + archetype, + entity, + bundle_info.iter_explicit_components(), + #[cfg(feature = "track_change_detection")] + caller, + ); } const QUERY_MISMATCH_ERROR: &str = "Query does not match the current entity"; @@ -3483,7 +3664,6 @@ where /// - [`OwningPtr`] and [`StorageType`] iterators must correspond to the /// [`BundleInfo`] used to construct [`BundleInserter`] /// - [`Entity`] must correspond to [`EntityLocation`] -#[track_caller] unsafe fn insert_dynamic_bundle< 'a, I: Iterator>, @@ -3494,6 +3674,7 @@ unsafe fn insert_dynamic_bundle< location: EntityLocation, components: I, storage_types: S, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, ) -> EntityLocation { struct DynamicInsertBundle<'a, I: Iterator)>> { components: I, @@ -3519,7 +3700,7 @@ unsafe fn insert_dynamic_bundle< bundle, InsertMode::Replace, #[cfg(feature = "track_change_detection")] - Location::caller(), + caller, ) } } @@ -3821,7 +4002,6 @@ unsafe impl DynamicComponentFetch for &'_ HashSet { mod tests { use bevy_ptr::{OwningPtr, Ptr}; use core::panic::AssertUnwindSafe; - #[cfg(feature = "track_change_detection")] use core::panic::Location; #[cfg(feature = "track_change_detection")] use std::sync::OnceLock; @@ -4903,12 +5083,22 @@ mod tests { #[component(on_add = ord_a_hook_on_add, on_insert = ord_a_hook_on_insert, on_replace = ord_a_hook_on_replace, on_remove = ord_a_hook_on_remove)] struct OrdA; - fn ord_a_hook_on_add(mut world: DeferredWorld, entity: Entity, _id: ComponentId) { + fn ord_a_hook_on_add( + mut world: DeferredWorld, + entity: Entity, + _id: ComponentId, + _caller: Option<&'static Location<'static>>, + ) { world.resource_mut::().0.push("OrdA hook on_add"); world.commands().entity(entity).insert(OrdB); } - fn ord_a_hook_on_insert(mut world: DeferredWorld, entity: Entity, _id: ComponentId) { + fn ord_a_hook_on_insert( + mut world: DeferredWorld, + entity: Entity, + _id: ComponentId, + _caller: Option<&'static Location<'static>>, + ) { world .resource_mut::() .0 @@ -4916,14 +5106,24 @@ mod tests { world.commands().entity(entity).despawn(); } - fn ord_a_hook_on_replace(mut world: DeferredWorld, _entity: Entity, _id: ComponentId) { + fn ord_a_hook_on_replace( + mut world: DeferredWorld, + _entity: Entity, + _id: ComponentId, + _caller: Option<&'static Location<'static>>, + ) { world .resource_mut::() .0 .push("OrdA hook on_replace"); } - fn ord_a_hook_on_remove(mut world: DeferredWorld, _entity: Entity, _id: ComponentId) { + fn ord_a_hook_on_remove( + mut world: DeferredWorld, + _entity: Entity, + _id: ComponentId, + _caller: Option<&'static Location<'static>>, + ) { world .resource_mut::() .0 @@ -4950,7 +5150,12 @@ mod tests { #[component(on_add = ord_b_hook_on_add, on_insert = ord_b_hook_on_insert, on_replace = ord_b_hook_on_replace, on_remove = ord_b_hook_on_remove)] struct OrdB; - fn ord_b_hook_on_add(mut world: DeferredWorld, _entity: Entity, _id: ComponentId) { + fn ord_b_hook_on_add( + mut world: DeferredWorld, + _entity: Entity, + _id: ComponentId, + _caller: Option<&'static Location<'static>>, + ) { world.resource_mut::().0.push("OrdB hook on_add"); world.commands().queue(|world: &mut World| { world @@ -4960,21 +5165,36 @@ mod tests { }); } - fn ord_b_hook_on_insert(mut world: DeferredWorld, _entity: Entity, _id: ComponentId) { + fn ord_b_hook_on_insert( + mut world: DeferredWorld, + _entity: Entity, + _id: ComponentId, + _caller: Option<&'static Location<'static>>, + ) { world .resource_mut::() .0 .push("OrdB hook on_insert"); } - fn ord_b_hook_on_replace(mut world: DeferredWorld, _entity: Entity, _id: ComponentId) { + fn ord_b_hook_on_replace( + mut world: DeferredWorld, + _entity: Entity, + _id: ComponentId, + _caller: Option<&'static Location<'static>>, + ) { world .resource_mut::() .0 .push("OrdB hook on_replace"); } - fn ord_b_hook_on_remove(mut world: DeferredWorld, _entity: Entity, _id: ComponentId) { + fn ord_b_hook_on_remove( + mut world: DeferredWorld, + _entity: Entity, + _id: ComponentId, + _caller: Option<&'static Location<'static>>, + ) { world .resource_mut::() .0 @@ -5114,7 +5334,7 @@ mod tests { struct C; static TRACKED: OnceLock<&'static Location<'static>> = OnceLock::new(); - fn get_tracked(world: DeferredWorld, entity: Entity, _: ComponentId) { + fn get_tracked(world: DeferredWorld, entity: Entity, _: ComponentId, _: Option<&Location>) { TRACKED.get_or_init(|| { world .entities diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index 4fe030ee29ced..a56669cee7f15 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -294,13 +294,19 @@ mod tests { ButtonState, InputPlugin, }; use bevy_window::WindowResolution; + use core::panic::Location; use smol_str::SmolStr; #[derive(Component)] #[component(on_add = set_focus_on_add)] struct SetFocusOnAdd; - fn set_focus_on_add(mut world: DeferredWorld, entity: Entity, _: ComponentId) { + fn set_focus_on_add( + mut world: DeferredWorld, + entity: Entity, + _: ComponentId, + _: Option<&Location>, + ) { world.set_input_focus(entity); } diff --git a/crates/bevy_input_focus/src/tab_navigation.rs b/crates/bevy_input_focus/src/tab_navigation.rs index 1bc7d300e7775..23c09cfd68416 100644 --- a/crates/bevy_input_focus/src/tab_navigation.rs +++ b/crates/bevy_input_focus/src/tab_navigation.rs @@ -26,6 +26,8 @@ //! //! This module also provides `AutoFocus`, a component which can be added to an entity to //! automatically focus it when it is added to the world. +use core::panic::Location; + use bevy_app::{App, Plugin, Startup}; use bevy_ecs::{ component::{Component, ComponentId}, @@ -292,7 +294,12 @@ pub fn handle_tab_navigation( } } -fn on_auto_focus_added(mut world: DeferredWorld, entity: Entity, _: ComponentId) { +fn on_auto_focus_added( + mut world: DeferredWorld, + entity: Entity, + _: ComponentId, + _: Option<&Location>, +) { if world.entity(entity).contains::() { world.set_input_focus(entity); } diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 96a13fd5d504d..48be23a84751c 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -38,6 +38,7 @@ use bevy_window::{ WindowScaleFactorChanged, }; use core::ops::Range; +use core::panic::Location; use derive_more::derive::From; use wgpu::{BlendState, TextureFormat, TextureUsages}; @@ -326,9 +327,14 @@ pub struct Camera { pub sub_camera_view: Option, } -fn warn_on_no_render_graph(world: DeferredWorld, entity: Entity, _: ComponentId) { +fn warn_on_no_render_graph( + world: DeferredWorld, + entity: Entity, + _: ComponentId, + caller: Option<&'static Location<'static>>, +) { if !world.entity(entity).contains::() { - warn!("Entity {entity} has a `Camera` component, but it doesn't have a render graph configured. Consider adding a `Camera2d` or `Camera3d` component, or manually adding a `CameraRenderGraph` component if you need a custom render graph."); + warn!("{}Entity {entity} has a `Camera` component, but it doesn't have a render graph configured. Consider adding a `Camera2d` or `Camera3d` component, or manually adding a `CameraRenderGraph` component if you need a custom render graph.", caller.map(|location|format!("{location}: ")).unwrap_or_default()); } } diff --git a/crates/bevy_render/src/sync_component.rs b/crates/bevy_render/src/sync_component.rs index 0fac10b409a17..f4f0f9a5e40fc 100644 --- a/crates/bevy_render/src/sync_component.rs +++ b/crates/bevy_render/src/sync_component.rs @@ -33,7 +33,7 @@ impl Plugin for SyncComponentPlugin { app.register_required_components::(); app.world_mut().register_component_hooks::().on_remove( - |mut world, entity, _component_id| { + |mut world, entity, _component_id, _caller| { let mut pending = world.resource_mut::(); pending.push(EntityRecord::ComponentRemoved(entity)); }, diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 3e991a6cbdde0..68df65503a35d 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -4,6 +4,7 @@ mod range; mod render_layers; use core::any::TypeId; +use core::panic::Location; use bevy_ecs::component::ComponentId; use bevy_ecs::entity::EntityHashSet; @@ -653,8 +654,12 @@ pub fn check_visibility( /// ... /// } /// ``` -pub fn add_visibility_class(mut world: DeferredWorld<'_>, entity: Entity, _: ComponentId) -where +pub fn add_visibility_class( + mut world: DeferredWorld<'_>, + entity: Entity, + _: ComponentId, + _: Option<&Location>, +) where C: 'static, { if let Some(mut visibility_class) = world.get_mut::(entity) { diff --git a/crates/bevy_scene/src/dynamic_scene.rs b/crates/bevy_scene/src/dynamic_scene.rs index babc45f7f119f..33a67596649da 100644 --- a/crates/bevy_scene/src/dynamic_scene.rs +++ b/crates/bevy_scene/src/dynamic_scene.rs @@ -368,7 +368,7 @@ mod tests { let mut dst_world = World::new(); dst_world .register_component_hooks::() - .on_add(|mut world, _, _| { + .on_add(|mut world, _, _, _| { world.commands().spawn_empty(); }); dst_world.insert_resource(reg.clone()); diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index 8a21b2040d78e..ea85ffc3c2707 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -71,7 +71,7 @@ impl Plugin for ScenePlugin { // Register component hooks for DynamicSceneRoot app.world_mut() .register_component_hooks::() - .on_remove(|mut world, entity, _| { + .on_remove(|mut world, entity, _, _| { let Some(handle) = world.get::(entity) else { return; }; @@ -90,7 +90,7 @@ impl Plugin for ScenePlugin { // Register component hooks for SceneRoot app.world_mut() .register_component_hooks::() - .on_remove(|mut world, entity, _| { + .on_remove(|mut world, entity, _, _| { if let Some(&SceneInstance(scene_instance)) = world.get::(entity) { let Some(mut scene_spawner) = world.get_resource_mut::() else { return; diff --git a/examples/ecs/component_hooks.rs b/examples/ecs/component_hooks.rs index 8786e1d12a98b..a2d33b8c1b12a 100644 --- a/examples/ecs/component_hooks.rs +++ b/examples/ecs/component_hooks.rs @@ -63,16 +63,22 @@ fn setup(world: &mut World) { world .register_component_hooks::() // There are 4 component lifecycle hooks: `on_add`, `on_insert`, `on_replace` and `on_remove` - // A hook has 3 arguments: + // A hook has 4 arguments: // - a `DeferredWorld`, this allows access to resource and component data as well as `Commands` // - the entity that triggered the hook // - the component id of the triggering component, this is mostly used for dynamic components + // - the location of the code that caused the hook to trigger // // `on_add` will trigger when a component is inserted onto an entity without it - .on_add(|mut world, entity, component_id| { + .on_add(|mut world, entity, component_id, caller| { // You can access component data from within the hook let value = world.get::(entity).unwrap().0; - println!("Component: {component_id:?} added to: {entity:?} with value {value:?}"); + println!( + "{component_id:?} added to {entity:?} with value {value:?}{}", + caller + .map(|location| format!("due to {location}")) + .unwrap_or_default() + ); // Or access resources world .resource_mut::() @@ -82,21 +88,26 @@ fn setup(world: &mut World) { }) // `on_insert` will trigger when a component is inserted onto an entity, // regardless of whether or not it already had it and after `on_add` if it ran - .on_insert(|world, _, _| { + .on_insert(|world, _, _, _| { println!("Current Index: {:?}", world.resource::()); }) // `on_replace` will trigger when a component is inserted onto an entity that already had it, // and runs before the value is replaced. // Also triggers when a component is removed from an entity, and runs before `on_remove` - .on_replace(|mut world, entity, _| { + .on_replace(|mut world, entity, _, _| { let value = world.get::(entity).unwrap().0; world.resource_mut::().remove(&value); }) // `on_remove` will trigger when a component is removed from an entity, // since it runs before the component is removed you can still access the component data - .on_remove(|mut world, entity, component_id| { + .on_remove(|mut world, entity, component_id, caller| { let value = world.get::(entity).unwrap().0; - println!("Component: {component_id:?} removed from: {entity:?} with value {value:?}"); + println!( + "{component_id:?} removed from {entity:?} with value {value:?}{}", + caller + .map(|location| format!("due to {location}")) + .unwrap_or_default() + ); // You can also issue commands through `.commands()` world.commands().entity(entity).despawn(); }); diff --git a/examples/ecs/immutable_components.rs b/examples/ecs/immutable_components.rs index 2e9b54522cd3d..fc480a0e894cb 100644 --- a/examples/ecs/immutable_components.rs +++ b/examples/ecs/immutable_components.rs @@ -10,6 +10,7 @@ use bevy::{ utils::HashMap, }; use core::alloc::Layout; +use core::panic::Location; /// This component is mutable, the default case. This is indicated by components /// implementing [`Component`] where [`Component::Mutability`] is [`Mutable`](bevy::ecs::component::Mutable). @@ -73,7 +74,12 @@ impl NameIndex { /// /// Since all mutations to [`Name`] are captured by hooks, we know it is not currently /// inserted in the index, and its value will not change without triggering a hook. -fn on_insert_name(mut world: DeferredWorld<'_>, entity: Entity, _component: ComponentId) { +fn on_insert_name( + mut world: DeferredWorld<'_>, + entity: Entity, + _component: ComponentId, + _caller: Option<&'static Location<'static>>, +) { let Some(&name) = world.entity(entity).get::() else { unreachable!("OnInsert hook guarantees `Name` is available on entity") }; @@ -88,7 +94,12 @@ fn on_insert_name(mut world: DeferredWorld<'_>, entity: Entity, _component: Comp /// /// Since all mutations to [`Name`] are captured by hooks, we know it is currently /// inserted in the index. -fn on_replace_name(mut world: DeferredWorld<'_>, entity: Entity, _component: ComponentId) { +fn on_replace_name( + mut world: DeferredWorld<'_>, + entity: Entity, + _component: ComponentId, + _caller: Option<&'static Location<'static>>, +) { let Some(&name) = world.entity(entity).get::() else { unreachable!("OnReplace hook guarantees `Name` is available on entity") }; From b2fbe410eeae4d24a8c6d86f168234a36178522f Mon Sep 17 00:00:00 2001 From: SpecificProtagonist Date: Tue, 17 Dec 2024 10:19:17 +0100 Subject: [PATCH 2/8] remove superfluous lifetimes in doctest --- crates/bevy_ecs/src/component.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index 4ea2c3d7da1b3..6fa5796719c53 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -311,12 +311,12 @@ pub use bevy_ecs_macros::require; /// // #[component(on_replace = my_on_replace_hook, on_remove = my_on_remove_hook)] /// struct ComponentA; /// -/// fn my_on_add_hook(world: DeferredWorld, entity: Entity, id: ComponentId, caller: Option<&'static Location<'static>>) { +/// fn my_on_add_hook(world: DeferredWorld, entity: Entity, id: ComponentId, caller: Option<&Location>) { /// // ... /// } /// /// // You can also omit writing some types using generics. -/// fn my_on_insert_hook(world: DeferredWorld, _: T1, _: T2, caller: Option<&'static Location<'static>>) { +/// fn my_on_insert_hook(world: DeferredWorld, _: T1, _: T2, caller: Option<&Location>) { /// // ... /// } /// ``` From f6151f6dd7c4da2aaab85986b8e01e8e87358ae7 Mon Sep 17 00:00:00 2001 From: SpecificProtagonist Date: Fri, 10 Jan 2025 01:09:53 +0100 Subject: [PATCH 3/8] ci --- crates/bevy_ecs/src/observer/mod.rs | 4 +++- crates/bevy_ecs/src/system/commands/mod.rs | 4 +--- crates/bevy_ecs/src/world/mod.rs | 9 ++++++++- crates/bevy_input_focus/src/tab_navigation.rs | 1 - 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index e0743263c1149..8cfaaf191785b 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -22,10 +22,12 @@ use core::{ fmt::Debug, marker::PhantomData, ops::{Deref, DerefMut}, - panic::Location, }; use smallvec::SmallVec; +#[cfg(feature = "track_location")] +use core::panic::Location; + /// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the /// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well. It also /// contains event propagation information. See [`Trigger::propagate`] for more information. diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 63dccbe0ca41a..5a12574b6f06a 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -15,10 +15,8 @@ pub use parallel_scope::*; use alloc::boxed::Box; use core::marker::PhantomData; -use log::error; - -#[cfg(feature = "track_location")] use core::panic::Location; +use log::error; use crate::{ self as bevy_ecs, diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index e3600ebce9997..0c0dc9aeca7f3 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1089,7 +1089,14 @@ impl World { let entity_location = { let mut bundle_spawner = BundleSpawner::new::(self, change_tick); // SAFETY: bundle's type matches `bundle_info`, entity is allocated but non-existent - unsafe { bundle_spawner.spawn_non_existent(entity, bundle, caller) } + unsafe { + bundle_spawner.spawn_non_existent( + entity, + bundle, + #[cfg(feature = "track_location")] + caller, + ) + } }; #[cfg(feature = "track_location")] diff --git a/crates/bevy_input_focus/src/tab_navigation.rs b/crates/bevy_input_focus/src/tab_navigation.rs index ab98073765dda..ef78b45a8f053 100644 --- a/crates/bevy_input_focus/src/tab_navigation.rs +++ b/crates/bevy_input_focus/src/tab_navigation.rs @@ -23,7 +23,6 @@ //! you can use the [`TabNavigation`] system parameter directly instead. //! This object can be injected into your systems, and provides a [`navigate`](`TabNavigation::navigate`) method which can be //! used to navigate between focusable entities. -use core::panic::Location; use bevy_app::{App, Plugin, Startup}; #[cfg(feature = "bevy_reflect")] From 7f25444ece3c2292904239cad1773778b193cce4 Mon Sep 17 00:00:00 2001 From: SpecificProtagonist Date: Fri, 10 Jan 2025 01:27:26 +0100 Subject: [PATCH 4/8] doclink --- crates/bevy_ecs/src/observer/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 8cfaaf191785b..d1b9835626e52 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -664,7 +664,7 @@ impl World { /// # Safety /// - /// See [`trigger_targets_dynamic_ref`] + /// See `trigger_targets_dynamic_ref` unsafe fn trigger_targets_dynamic_ref_with_caller( &mut self, event_id: ComponentId, From 4918e717ae8dc398517602890de2a50e38a6b4ef Mon Sep 17 00:00:00 2001 From: SpecificProtagonist Date: Sun, 19 Jan 2025 18:03:53 +0100 Subject: [PATCH 5/8] hierarchy --- crates/bevy_ecs/src/hierarchy.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/hierarchy.rs b/crates/bevy_ecs/src/hierarchy.rs index 6e495a116b7f9..c20de3cda653e 100644 --- a/crates/bevy_ecs/src/hierarchy.rs +++ b/crates/bevy_ecs/src/hierarchy.rs @@ -22,8 +22,8 @@ use crate::{ }; use alloc::{format, string::String, vec::Vec}; use bevy_ecs_macros::VisitEntitiesMut; -use core::ops::Deref; use core::slice; +use core::{ops::Deref, panic::Location}; use disqualified::ShortName; use log::warn; @@ -270,6 +270,7 @@ pub fn validate_parent_has_component( world: DeferredWorld, entity: Entity, _: ComponentId, + caller: Option<&'static Location<'static>>, ) { let entity_ref = world.entity(entity); let Some(child_of) = entity_ref.get::() else { @@ -282,8 +283,9 @@ pub fn validate_parent_has_component( // TODO: print name here once Name lives in bevy_ecs let name: Option = None; warn!( - "warning[B0004]: {name} with the {ty_name} component has a parent without {ty_name}.\n\ + "{}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", + caller.map(|c| format!("{c}: ")).unwrap_or_default(), ty_name = ShortName::of::(), name = name.map_or_else( || format!("Entity {}", entity), From 0f1c177547725d464bb873ae9aca0993806ace8f Mon Sep 17 00:00:00 2001 From: SpecificProtagonist Date: Sun, 19 Jan 2025 18:58:37 +0100 Subject: [PATCH 6/8] fix test --- crates/bevy_ecs/src/observer/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index ebf261501715e..e6f56bfaaf47a 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -1625,7 +1625,7 @@ mod tests { assert_eq!(trigger.trigger.caller, caller); }); world.commands().spawn(Component).clear(); - world.flush_commands(); + world.flush(); } #[test] From 8933b9503f168a6132f43d02de69afd8442a8f51 Mon Sep 17 00:00:00 2001 From: SpecificProtagonist Date: Sun, 19 Jan 2025 21:23:18 +0100 Subject: [PATCH 7/8] Make trigger caller publicly accessible --- crates/bevy_ecs/src/observer/mod.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index e6f56bfaaf47a..38692bf381705 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -141,6 +141,12 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> { pub fn get_propagate(&self) -> bool { *self.propagate } + + /// Returns the source code location that triggered this observer. + #[cfg(feature = "track_location")] + pub fn caller(&self) -> &'static Location<'static> { + self.trigger.caller + } } impl<'w, E: Debug, B: Bundle> Debug for Trigger<'w, E, B> { @@ -1604,7 +1610,7 @@ mod tests { let caller = Location::caller(); let mut world = World::new(); world.add_observer(move |trigger: Trigger| { - assert_eq!(trigger.trigger.caller, caller); + assert_eq!(trigger.caller(), caller); }); world.trigger(EventA); } @@ -1619,10 +1625,10 @@ mod tests { let caller = Location::caller(); let mut world = World::new(); world.add_observer(move |trigger: Trigger| { - assert_eq!(trigger.trigger.caller, caller); + assert_eq!(trigger.caller(), caller); }); world.add_observer(move |trigger: Trigger| { - assert_eq!(trigger.trigger.caller, caller); + assert_eq!(trigger.caller(), caller); }); world.commands().spawn(Component).clear(); world.flush(); From f34cc9f159a660ad70d9bee2d69cd110292f73a5 Mon Sep 17 00:00:00 2001 From: SpecificProtagonist Date: Wed, 22 Jan 2025 16:40:25 +0100 Subject: [PATCH 8/8] address review comments --- crates/bevy_ecs/src/hierarchy.rs | 2 +- crates/bevy_ecs/src/world/entity_ref.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/hierarchy.rs b/crates/bevy_ecs/src/hierarchy.rs index c20de3cda653e..5439613cbb369 100644 --- a/crates/bevy_ecs/src/hierarchy.rs +++ b/crates/bevy_ecs/src/hierarchy.rs @@ -283,7 +283,7 @@ pub fn validate_parent_has_component( // TODO: print name here once Name lives in bevy_ecs let name: Option = None; warn!( - "{}warning[B0004]: {name} with the {ty_name} component has a parent without {ty_name}.\n\ + "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", caller.map(|c| format!("{c}: ")).unwrap_or_default(), ty_name = ShortName::of::(), diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 57d9cfd55b027..da2f72dee92fa 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -2196,7 +2196,6 @@ impl<'w> EntityWorldMut<'w> { #[inline] pub(crate) fn clear_with_caller( &mut self, - #[cfg(feature = "track_location")] caller: &'static Location<'static>, ) -> &mut Self { self.assert_not_despawned();