Track callsite for observers & hooks (#15607)

# Objective

Fixes #14708

Also fixes some commands not updating tracked location.


## Solution

`ObserverTrigger` has a new `caller` field with the
`track_change_detection` feature;
hooks take an additional caller parameter (which is `Some(…)` or `None`
depending on the feature).

## Testing

See the new tests in `src/observer/mod.rs`

---

## Showcase

Observers now know from where they were triggered (if
`track_change_detection` is enabled):
```rust
world.observe(move |trigger: Trigger<OnAdd, Foo>| {
    println!("Added Foo from {}", trigger.caller());
});
```

## Migration

- hooks now take an additional `Option<&'static Location>` argument

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
SpecificProtagonist 2025-01-22 21:02:39 +01:00 committed by GitHub
parent 5c43890d49
commit f32a6fb205
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 789 additions and 133 deletions

View File

@ -70,10 +70,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::<OrderIndependentTransparencySettings>(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
);
}
}
});

View File

@ -1058,12 +1058,16 @@ impl<'w> BundleInserter<'w> {
ON_REPLACE,
entity,
archetype_after_insert.iter_existing(),
#[cfg(feature = "track_location")]
caller,
);
}
deferred_world.trigger_on_replace(
archetype,
entity,
archetype_after_insert.iter_existing(),
#[cfg(feature = "track_location")]
caller,
);
}
}
@ -1236,12 +1240,16 @@ impl<'w> BundleInserter<'w> {
new_archetype,
entity,
archetype_after_insert.iter_added(),
#[cfg(feature = "track_location")]
caller,
);
if new_archetype.has_add_observer() {
deferred_world.trigger_observers(
ON_ADD,
entity,
archetype_after_insert.iter_added(),
#[cfg(feature = "track_location")]
caller,
);
}
match insert_mode {
@ -1251,12 +1259,16 @@ impl<'w> BundleInserter<'w> {
new_archetype,
entity,
archetype_after_insert.iter_inserted(),
#[cfg(feature = "track_location")]
caller,
);
if new_archetype.has_insert_observer() {
deferred_world.trigger_observers(
ON_INSERT,
entity,
archetype_after_insert.iter_inserted(),
#[cfg(feature = "track_location")]
caller,
);
}
}
@ -1267,12 +1279,16 @@ impl<'w> BundleInserter<'w> {
new_archetype,
entity,
archetype_after_insert.iter_added(),
#[cfg(feature = "track_location")]
caller,
);
if new_archetype.has_insert_observer() {
deferred_world.trigger_observers(
ON_INSERT,
entity,
archetype_after_insert.iter_added(),
#[cfg(feature = "track_location")]
caller,
);
}
}
@ -1348,6 +1364,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<T: DynamicBundle>(
&mut self,
entity: Entity,
@ -1395,24 +1412,32 @@ impl<'w> BundleSpawner<'w> {
archetype,
entity,
bundle_info.iter_contributed_components(),
#[cfg(feature = "track_location")]
caller,
);
if archetype.has_add_observer() {
deferred_world.trigger_observers(
ON_ADD,
entity,
bundle_info.iter_contributed_components(),
#[cfg(feature = "track_location")]
caller,
);
}
deferred_world.trigger_on_insert(
archetype,
entity,
bundle_info.iter_contributed_components(),
#[cfg(feature = "track_location")]
caller,
);
if archetype.has_insert_observer() {
deferred_world.trigger_observers(
ON_INSERT,
entity,
bundle_info.iter_contributed_components(),
#[cfg(feature = "track_location")]
caller,
);
}
};
@ -1681,6 +1706,7 @@ mod tests {
use crate as bevy_ecs;
use crate::{component::ComponentId, prelude::*, world::DeferredWorld};
use alloc::vec;
use core::panic::Location;
#[derive(Component)]
struct A;
@ -1689,19 +1715,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::<R>().assert_order(0);
}
fn a_on_insert<T1, T2>(mut world: DeferredWorld, _: T1, _: T2) {
fn a_on_insert<T1, T2>(
mut world: DeferredWorld,
_: T1,
_: T2,
_: Option<&'static Location<'static>>,
) {
world.resource_mut::<R>().assert_order(1);
}
fn a_on_replace<T1, T2>(mut world: DeferredWorld, _: T1, _: T2) {
fn a_on_replace<T1, T2>(
mut world: DeferredWorld,
_: T1,
_: T2,
_: Option<&'static Location<'static>>,
) {
world.resource_mut::<R>().assert_order(2);
}
fn a_on_remove<T1, T2>(mut world: DeferredWorld, _: T1, _: T2) {
fn a_on_remove<T1, T2>(
mut world: DeferredWorld,
_: T1,
_: T2,
_: Option<&'static Location<'static>>,
) {
world.resource_mut::<R>().assert_order(3);
}
@ -1734,10 +1780,10 @@ mod tests {
world.init_resource::<R>();
world
.register_component_hooks::<A>()
.on_add(|mut world, _, _| world.resource_mut::<R>().assert_order(0))
.on_insert(|mut world, _, _| world.resource_mut::<R>().assert_order(1))
.on_replace(|mut world, _, _| world.resource_mut::<R>().assert_order(2))
.on_remove(|mut world, _, _| world.resource_mut::<R>().assert_order(3));
.on_add(|mut world, _, _, _| world.resource_mut::<R>().assert_order(0))
.on_insert(|mut world, _, _, _| world.resource_mut::<R>().assert_order(1))
.on_replace(|mut world, _, _, _| world.resource_mut::<R>().assert_order(2))
.on_remove(|mut world, _, _, _| world.resource_mut::<R>().assert_order(3));
let entity = world.spawn(A).id();
world.despawn(entity);
@ -1761,10 +1807,10 @@ mod tests {
world.init_resource::<R>();
world
.register_component_hooks::<A>()
.on_add(|mut world, _, _| world.resource_mut::<R>().assert_order(0))
.on_insert(|mut world, _, _| world.resource_mut::<R>().assert_order(1))
.on_replace(|mut world, _, _| world.resource_mut::<R>().assert_order(2))
.on_remove(|mut world, _, _| world.resource_mut::<R>().assert_order(3));
.on_add(|mut world, _, _, _| world.resource_mut::<R>().assert_order(0))
.on_insert(|mut world, _, _, _| world.resource_mut::<R>().assert_order(1))
.on_replace(|mut world, _, _, _| world.resource_mut::<R>().assert_order(2))
.on_remove(|mut world, _, _, _| world.resource_mut::<R>().assert_order(3));
let mut entity = world.spawn_empty();
entity.insert(A);
@ -1778,8 +1824,8 @@ mod tests {
let mut world = World::new();
world
.register_component_hooks::<A>()
.on_replace(|mut world, _, _| world.resource_mut::<R>().assert_order(0))
.on_insert(|mut world, _, _| {
.on_replace(|mut world, _, _, _| world.resource_mut::<R>().assert_order(0))
.on_insert(|mut world, _, _, _| {
if let Some(mut r) = world.get_resource_mut::<R>() {
r.assert_order(1);
}
@ -1800,22 +1846,22 @@ mod tests {
world.init_resource::<R>();
world
.register_component_hooks::<A>()
.on_add(|mut world, entity, _| {
.on_add(|mut world, entity, _, _| {
world.resource_mut::<R>().assert_order(0);
world.commands().entity(entity).insert(B);
})
.on_remove(|mut world, entity, _| {
.on_remove(|mut world, entity, _, _| {
world.resource_mut::<R>().assert_order(2);
world.commands().entity(entity).remove::<B>();
});
world
.register_component_hooks::<B>()
.on_add(|mut world, entity, _| {
.on_add(|mut world, entity, _, _| {
world.resource_mut::<R>().assert_order(1);
world.commands().entity(entity).remove::<A>();
})
.on_remove(|mut world, _, _| {
.on_remove(|mut world, _, _, _| {
world.resource_mut::<R>().assert_order(3);
});
@ -1832,27 +1878,27 @@ mod tests {
world.init_resource::<R>();
world
.register_component_hooks::<A>()
.on_add(|mut world, entity, _| {
.on_add(|mut world, entity, _, _| {
world.resource_mut::<R>().assert_order(0);
world.commands().entity(entity).insert(B).insert(C);
});
world
.register_component_hooks::<B>()
.on_add(|mut world, entity, _| {
.on_add(|mut world, entity, _, _| {
world.resource_mut::<R>().assert_order(1);
world.commands().entity(entity).insert(D);
});
world
.register_component_hooks::<C>()
.on_add(|mut world, _, _| {
.on_add(|mut world, _, _, _| {
world.resource_mut::<R>().assert_order(3);
});
world
.register_component_hooks::<D>()
.on_add(|mut world, _, _| {
.on_add(|mut world, _, _, _| {
world.resource_mut::<R>().assert_order(2);
});

View File

@ -21,8 +21,6 @@ use bevy_ptr::{OwningPtr, UnsafeCellDeref};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
use bevy_utils::{HashMap, HashSet, TypeIdMap};
#[cfg(feature = "track_location")]
use core::panic::Location;
use core::{
alloc::Layout,
any::{Any, TypeId},
@ -30,6 +28,7 @@ use core::{
fmt::Debug,
marker::PhantomData,
mem::needs_drop,
panic::Location,
};
use disqualified::ShortName;
use thiserror::Error;
@ -304,6 +303,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)]
@ -315,12 +315,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<&Location>) {
/// // ...
/// }
///
/// // You can also omit writing some types using generics.
/// fn my_on_insert_hook<T1, T2>(world: DeferredWorld, _: T1, _: T2) {
/// fn my_on_insert_hook<T1, T2>(world: DeferredWorld, _: T1, _: T2, caller: Option<&Location>) {
/// // ...
/// }
/// ```
@ -497,8 +497,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_caller` 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`].
///
@ -535,12 +537,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::<MyTrackedComponent>().on_add(|mut world, entity, _component_id| {
/// world.register_component_hooks::<MyTrackedComponent>().on_add(|mut world, entity, _component_id, _caller| {
/// let mut tracked_entities = world.resource_mut::<TrackedEntities>();
/// tracked_entities.0.insert(entity);
/// });
///
/// world.register_component_hooks::<MyTrackedComponent>().on_remove(|mut world, entity, _component_id| {
/// world.register_component_hooks::<MyTrackedComponent>().on_remove(|mut world, entity, _component_id, _caller| {
/// let mut tracked_entities = world.resource_mut::<TrackedEntities>();
/// tracked_entities.0.remove(&entity);
/// });

View File

@ -274,6 +274,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) {
// SAFETY:
// - `source_entity` is read-only.

View File

@ -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<C: Component>(
world: DeferredWorld,
entity: Entity,
_: ComponentId,
caller: Option<&'static Location<'static>>,
) {
let entity_ref = world.entity(entity);
let Some(child_of) = entity_ref.get::<ChildOf>() else {
@ -282,8 +283,9 @@ pub fn validate_parent_has_component<C: Component>(
// TODO: print name here once Name lives in bevy_ecs
let name: Option<String> = 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::<C>(),
name = name.map_or_else(
|| format!("Entity {}", entity),

View File

@ -2030,8 +2030,8 @@ mod tests {
world.insert_resource(I(0));
world
.register_component_hooks::<Y>()
.on_add(|mut world, _, _| world.resource_mut::<A>().0 += 1)
.on_insert(|mut world, _, _| world.resource_mut::<I>().0 += 1);
.on_add(|mut world, _, _, _| world.resource_mut::<A>().0 += 1)
.on_insert(|mut world, _, _, _| world.resource_mut::<I>().0 += 1);
// Spawn entity and ensure Y was added
assert!(world.spawn(X).contains::<Y>());
@ -2060,8 +2060,8 @@ mod tests {
world.insert_resource(I(0));
world
.register_component_hooks::<Y>()
.on_add(|mut world, _, _| world.resource_mut::<A>().0 += 1)
.on_insert(|mut world, _, _| world.resource_mut::<I>().0 += 1);
.on_add(|mut world, _, _, _| world.resource_mut::<A>().0 += 1)
.on_insert(|mut world, _, _, _| world.resource_mut::<I>().0 += 1);
// Spawn entity and ensure Y was added
assert!(world.spawn_empty().insert(X).contains::<Y>());

View File

@ -15,7 +15,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::<ObservedBy>(entity).unwrap();
core::mem::take(&mut component.0)

View File

@ -24,6 +24,9 @@ use core::{
};
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.
@ -138,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> {
@ -311,6 +320,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_location")]
pub caller: &'static Location<'static>,
}
impl ObserverTrigger {
@ -387,6 +400,7 @@ impl Observers {
components: impl Iterator<Item = ComponentId> + Clone,
data: &mut T,
propagate: &mut bool,
#[cfg(feature = "track_location")] caller: &'static Location<'static>,
) {
// SAFETY: You cannot get a mutable reference to `observers` from `DeferredWorld`
let (mut world, observers) = unsafe {
@ -411,6 +425,8 @@ impl Observers {
event_type,
components: components.clone().collect(),
target,
#[cfg(feature = "track_location")]
caller,
},
data.into(),
propagate,
@ -532,16 +548,38 @@ 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.
pub fn trigger<E: Event>(&mut self, mut event: E) {
#[track_caller]
pub fn trigger<E: Event>(&mut self, event: E) {
self.trigger_with_caller(
event,
#[cfg(feature = "track_location")]
Location::caller(),
);
}
pub(crate) fn trigger_with_caller<E: Event>(
&mut self,
mut event: E,
#[cfg(feature = "track_location")] caller: &'static Location<'static>,
) {
let event_id = E::register_component_id(self);
// SAFETY: We just registered `event_id` with the type of `event`
unsafe { self.trigger_targets_dynamic_ref(event_id, &mut event, ()) };
unsafe {
self.trigger_targets_dynamic_ref_with_caller(
event_id,
&mut event,
(),
#[cfg(feature = "track_location")]
caller,
);
}
}
/// 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<E: Event>(&mut self, event: &mut E) {
let event_id = E::register_component_id(self);
// SAFETY: We just registered `event_id` with the type of `event`
@ -553,10 +591,33 @@ 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.
pub fn trigger_targets<E: Event>(&mut self, mut event: E, targets: impl TriggerTargets) {
#[track_caller]
pub fn trigger_targets<E: Event>(&mut self, event: E, targets: impl TriggerTargets) {
self.trigger_targets_with_caller(
event,
targets,
#[cfg(feature = "track_location")]
Location::caller(),
);
}
pub(crate) fn trigger_targets_with_caller<E: Event>(
&mut self,
mut event: E,
targets: impl TriggerTargets,
#[cfg(feature = "track_location")] caller: &'static Location<'static>,
) {
let event_id = E::register_component_id(self);
// SAFETY: We just registered `event_id` with the type of `event`
unsafe { self.trigger_targets_dynamic_ref(event_id, &mut event, targets) };
unsafe {
self.trigger_targets_dynamic_ref_with_caller(
event_id,
&mut event,
targets,
#[cfg(feature = "track_location")]
caller,
);
}
}
/// Triggers the given [`Event`] as a mutable reference for the given `targets`,
@ -564,6 +625,7 @@ 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<E: Event>(&mut self, event: &mut E, targets: impl TriggerTargets) {
let event_id = E::register_component_id(self);
// SAFETY: We just registered `event_id` with the type of `event`
@ -579,6 +641,7 @@ impl World {
/// # Safety
///
/// Caller must ensure that `event_data` is accessible as the type represented by `event_id`.
#[track_caller]
pub unsafe fn trigger_targets_dynamic<E: Event, Targets: TriggerTargets>(
&mut self,
event_id: ComponentId,
@ -600,11 +663,31 @@ impl World {
/// # Safety
///
/// Caller must ensure that `event_data` is accessible as the type represented by `event_id`.
#[track_caller]
pub unsafe fn trigger_targets_dynamic_ref<E: Event, Targets: TriggerTargets>(
&mut self,
event_id: ComponentId,
event_data: &mut E,
targets: Targets,
) {
self.trigger_targets_dynamic_ref_with_caller(
event_id,
event_data,
targets,
#[cfg(feature = "track_location")]
Location::caller(),
);
}
/// # Safety
///
/// See `trigger_targets_dynamic_ref`
unsafe fn trigger_targets_dynamic_ref_with_caller<E: Event, Targets: TriggerTargets>(
&mut self,
event_id: ComponentId,
event_data: &mut E,
targets: Targets,
#[cfg(feature = "track_location")] caller: &'static Location<'static>,
) {
let mut world = DeferredWorld::from(self);
if targets.entities().is_empty() {
@ -616,6 +699,8 @@ impl World {
targets.components(),
event_data,
false,
#[cfg(feature = "track_location")]
caller,
);
};
} else {
@ -628,6 +713,8 @@ impl World {
targets.components(),
event_data,
E::AUTO_PROPAGATE,
#[cfg(feature = "track_location")]
caller,
);
};
}
@ -756,6 +843,8 @@ impl World {
#[cfg(test)]
mod tests {
use alloc::{vec, vec::Vec};
#[cfg(feature = "track_location")]
use core::panic::Location;
use bevy_ptr::OwningPtr;
use bevy_utils::HashMap;
@ -1511,6 +1600,40 @@ mod tests {
assert!(world.get_resource::<ResA>().is_some());
}
#[test]
#[cfg(feature = "track_location")]
#[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<EventA>| {
assert_eq!(trigger.caller(), caller);
});
world.trigger(EventA);
}
#[test]
#[cfg(feature = "track_location")]
#[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<OnAdd, Component>| {
assert_eq!(trigger.caller(), caller);
});
world.add_observer(move |trigger: Trigger<OnRemove, Component>| {
assert_eq!(trigger.caller(), caller);
});
world.commands().spawn(Component).clear();
world.flush();
}
#[test]
fn observer_triggered_components() {
#[derive(Resource, Default)]

View File

@ -1,5 +1,6 @@
use alloc::{boxed::Box, vec, vec::Vec};
use core::any::Any;
use core::panic::Location;
use crate::{
component::{ComponentHook, ComponentHooks, ComponentId, Mutable, StorageType},
@ -66,12 +67,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)
@ -318,12 +319,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::<Self>(entity) else {
return;
};
let hook = observe.hook_on_add;
hook(world, entity, _id);
hook(world, entity, id, caller);
});
}
}
@ -396,6 +397,7 @@ fn hook_on_add<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
mut world: DeferredWorld<'_>,
entity: Entity,
_: ComponentId,
_: Option<&'static Location<'static>>,
) {
world.commands().queue(move |world: &mut World| {
let event_id = E::register_component_id(world);

View File

@ -4,6 +4,9 @@ mod related_methods;
mod relationship_query;
mod relationship_source_collection;
use alloc::format;
use core::panic::Location;
pub use related_methods::*;
pub use relationship_query::*;
pub use relationship_source_collection::*;
@ -71,11 +74,17 @@ pub trait Relationship: Component + Sized {
fn from(entity: Entity) -> Self;
/// The `on_insert` component hook that maintains the [`Relationship`] / [`RelationshipTarget`] connection.
fn on_insert(mut world: DeferredWorld, entity: Entity, _: ComponentId) {
fn on_insert(
mut world: DeferredWorld,
entity: Entity,
_: ComponentId,
caller: Option<&'static Location<'static>>,
) {
let target_entity = world.entity(entity).get::<Self>().unwrap().get();
if target_entity == entity {
warn!(
"The {}({target_entity:?}) relationship on entity {entity:?} points to itself. The invalid {} relationship has been removed.",
"{}The {}({target_entity:?}) relationship on entity {entity:?} points to itself. The invalid {} relationship has been removed.",
caller.map(|location|format!("{location}: ")).unwrap_or_default(),
core::any::type_name::<Self>(),
core::any::type_name::<Self>()
);
@ -93,7 +102,8 @@ pub trait Relationship: Component + Sized {
}
} else {
warn!(
"The {}({target_entity:?}) relationship on entity {entity:?} relates to an entity that does not exist. The invalid {} relationship has been removed.",
"{}The {}({target_entity:?}) relationship on entity {entity:?} relates to an entity that does not exist. The invalid {} relationship has been removed.",
caller.map(|location|format!("{location}: ")).unwrap_or_default(),
core::any::type_name::<Self>(),
core::any::type_name::<Self>()
);
@ -103,7 +113,12 @@ pub trait Relationship: Component + Sized {
/// The `on_replace` component hook that maintains the [`Relationship`] / [`RelationshipTarget`] connection.
// note: think of this as "on_drop"
fn on_replace(mut world: DeferredWorld, entity: Entity, _: ComponentId) {
fn on_replace(
mut world: DeferredWorld,
entity: Entity,
_: ComponentId,
_: Option<&'static Location<'static>>,
) {
let target_entity = world.entity(entity).get::<Self>().unwrap().get();
if let Ok(mut target_entity_mut) = world.get_entity_mut(target_entity) {
if let Some(mut relationship_target) =
@ -164,7 +179,12 @@ pub trait RelationshipTarget: Component<Mutability = Mutable> + Sized {
/// The `on_replace` component hook that maintains the [`Relationship`] / [`RelationshipTarget`] connection.
// note: think of this as "on_drop"
fn on_replace(mut world: DeferredWorld, entity: Entity, _: ComponentId) {
fn on_replace(
mut world: DeferredWorld,
entity: Entity,
_: ComponentId,
caller: Option<&'static Location<'static>>,
) {
// NOTE: this unsafe code is an optimization. We could make this safe, but it would require
// copying the RelationshipTarget collection
// SAFETY: This only reads the Self component and queues Remove commands
@ -180,7 +200,13 @@ pub trait RelationshipTarget: Component<Mutability = Mutable> + Sized {
.handle_error_with(error_handler::silent()),
);
} else {
warn!("Tried to despawn non-existent entity {}", source_entity);
warn!(
"{}Tried to despawn non-existent entity {}",
caller
.map(|location| format!("{location}: "))
.unwrap_or_default(),
source_entity
);
}
}
}
@ -189,7 +215,12 @@ pub trait RelationshipTarget: Component<Mutability = Mutable> + Sized {
/// The `on_despawn` component hook that despawns entities stored in an entity's [`RelationshipTarget`] when
/// that entity is despawned.
// note: think of this as "on_drop"
fn on_despawn(mut world: DeferredWorld, entity: Entity, _: ComponentId) {
fn on_despawn(
mut world: DeferredWorld,
entity: Entity,
_: ComponentId,
caller: Option<&'static Location<'static>>,
) {
// NOTE: this unsafe code is an optimization. We could make this safe, but it would require
// copying the RelationshipTarget collection
// SAFETY: This only reads the Self component and queues despawn commands
@ -205,7 +236,13 @@ pub trait RelationshipTarget: Component<Mutability = Mutable> + Sized {
.handle_error_with(error_handler::silent()),
);
} else {
warn!("Tried to despawn non-existent entity {}", source_entity);
warn!(
"{}Tried to despawn non-existent entity {}",
caller
.map(|location| format!("{location}: "))
.unwrap_or_default(),
source_entity
);
}
}
}

View File

@ -265,9 +265,16 @@ pub fn run_schedule(label: impl ScheduleLabel) -> impl Command<Result> {
}
/// A [`Command`] that sends a global [`Trigger`](crate::observer::Trigger) without any targets.
#[track_caller]
pub fn trigger(event: impl Event) -> impl Command {
#[cfg(feature = "track_location")]
let caller = Location::caller();
move |world: &mut World| {
world.trigger(event);
world.trigger_with_caller(
event,
#[cfg(feature = "track_location")]
caller,
);
}
}
@ -276,8 +283,15 @@ pub fn trigger_targets(
event: impl Event,
targets: impl TriggerTargets + Send + Sync + 'static,
) -> impl Command {
#[cfg(feature = "track_location")]
let caller = Location::caller();
move |world: &mut World| {
world.trigger_targets(event, targets);
world.trigger_targets_with_caller(
event,
targets,
#[cfg(feature = "track_location")]
caller,
);
}
}

View File

@ -186,12 +186,19 @@ pub fn insert_if_new(bundle: impl Bundle) -> impl EntityCommand {
/// An [`EntityCommand`] that adds a dynamic component to an entity.
#[track_caller]
pub fn insert_by_id<T: Send + 'static>(component_id: ComponentId, value: T) -> impl EntityCommand {
#[cfg(feature = "track_location")]
let caller = Location::caller();
move |mut entity: EntityWorldMut| {
// 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_location")]
caller,
);
});
}
}
@ -214,39 +221,70 @@ pub fn insert_from_world<T: Component + FromWorld>(mode: InsertMode) -> impl Ent
}
/// An [`EntityCommand`] that removes the components in a [`Bundle`] from an entity.
#[track_caller]
pub fn remove<T: Bundle>() -> impl EntityCommand {
#[cfg(feature = "track_location")]
let caller = Location::caller();
move |mut entity: EntityWorldMut| {
entity.remove::<T>();
entity.remove_with_caller::<T>(
#[cfg(feature = "track_location")]
caller,
);
}
}
/// An [`EntityCommand`] that removes the components in a [`Bundle`] from an entity,
/// as well as the required components for each component removed.
#[track_caller]
pub fn remove_with_requires<T: Bundle>() -> impl EntityCommand {
#[cfg(feature = "track_location")]
let caller = Location::caller();
move |mut entity: EntityWorldMut| {
entity.remove_with_requires::<T>();
entity.remove_with_requires_with_caller::<T>(
#[cfg(feature = "track_location")]
caller,
);
}
}
/// An [`EntityCommand`] that removes a dynamic component from an entity.
#[track_caller]
pub fn remove_by_id(component_id: ComponentId) -> impl EntityCommand {
#[cfg(feature = "track_location")]
let caller = Location::caller();
move |mut entity: EntityWorldMut| {
entity.remove_by_id(component_id);
entity.remove_by_id_with_caller(
component_id,
#[cfg(feature = "track_location")]
caller,
);
}
}
/// An [`EntityCommand`] that removes all components from an entity.
#[track_caller]
pub fn clear() -> impl EntityCommand {
#[cfg(feature = "track_location")]
let caller = Location::caller();
move |mut entity: EntityWorldMut| {
entity.clear();
entity.clear_with_caller(
#[cfg(feature = "track_location")]
caller,
);
}
}
/// An [`EntityCommand`] that removes all components from an entity,
/// except for those in the given [`Bundle`].
#[track_caller]
pub fn retain<T: Bundle>() -> impl EntityCommand {
#[cfg(feature = "track_location")]
let caller = Location::caller();
move |mut entity: EntityWorldMut| {
entity.retain::<T>();
entity.retain_with_caller::<T>(
#[cfg(feature = "track_location")]
caller,
);
}
}
@ -256,6 +294,7 @@ pub fn retain<T: Bundle>() -> impl EntityCommand {
///
/// This will also despawn any [`Children`](crate::hierarchy::Children) entities, and any other [`RelationshipTarget`](crate::relationship::RelationshipTarget) that is configured
/// to despawn descendants. This results in "recursive despawn" behavior.
#[track_caller]
pub fn despawn() -> impl EntityCommand {
#[cfg(feature = "track_location")]
let caller = Location::caller();
@ -269,11 +308,18 @@ pub fn despawn() -> impl EntityCommand {
/// An [`EntityCommand`] that creates an [`Observer`](crate::observer::Observer)
/// listening for events of type `E` targeting an entity
#[track_caller]
pub fn observe<E: Event, B: Bundle, M>(
observer: impl IntoObserverSystem<E, B, M>,
) -> impl EntityCommand {
#[cfg(feature = "track_location")]
let caller = Location::caller();
move |mut entity: EntityWorldMut| {
entity.observe(observer);
entity.observe_with_caller(
observer,
#[cfg(feature = "track_location")]
caller,
);
}
}

View File

@ -13,10 +13,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,
@ -685,7 +683,6 @@ impl<'w, 's> Commands<'w, 's> {
I: IntoIterator<Item = (Entity, B)> + Send + Sync + 'static,
B: Bundle,
{
#[cfg(feature = "track_location")]
let caller = Location::caller();
self.queue(move |world: &mut World| {
if let Err(invalid_entities) = world.insert_or_spawn_batch_with_caller(
@ -694,7 +691,7 @@ impl<'w, 's> Commands<'w, 's> {
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::<B>(),
invalid_entities
);
@ -1044,6 +1041,7 @@ 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(command::trigger(event));
}
@ -1052,6 +1050,7 @@ impl<'w, 's> Commands<'w, 's> {
/// watches those targets.
///
/// [`Trigger`]: crate::observer::Trigger
#[track_caller]
pub fn trigger_targets(
&mut self,
event: impl Event,
@ -1602,6 +1601,7 @@ impl<'a> EntityCommands<'a> {
/// }
/// # bevy_ecs::system::assert_is_system(remove_combat_stats_system);
/// ```
#[track_caller]
pub fn remove<T>(&mut self) -> &mut Self
where
T: Bundle,
@ -1679,6 +1679,7 @@ impl<'a> EntityCommands<'a> {
/// }
/// # bevy_ecs::system::assert_is_system(remove_with_requires_system);
/// ```
#[track_caller]
pub fn remove_with_requires<T: Bundle>(&mut self) -> &mut Self {
self.queue(entity_command::remove_with_requires::<T>())
}
@ -1688,11 +1689,13 @@ impl<'a> EntityCommands<'a> {
/// # Panics
///
/// Panics if the provided [`ComponentId`] does not exist in the [`World`].
#[track_caller]
pub fn remove_by_id(&mut self, component_id: ComponentId) -> &mut Self {
self.queue(entity_command::remove_by_id(component_id))
}
/// Removes all components associated with the entity.
#[track_caller]
pub fn clear(&mut self) -> &mut Self {
self.queue(entity_command::clear())
}
@ -1865,6 +1868,7 @@ impl<'a> EntityCommands<'a> {
/// }
/// # bevy_ecs::system::assert_is_system(remove_combat_stats_system);
/// ```
#[track_caller]
pub fn retain<T>(&mut self) -> &mut Self
where
T: Bundle,

View File

@ -1,4 +1,6 @@
use core::ops::Deref;
#[cfg(feature = "track_location")]
use core::panic::Location;
use crate::{
archetype::Archetype,
@ -126,9 +128,21 @@ impl<'w> DeferredWorld<'w> {
// - ON_REPLACE is able to accept ZST events
unsafe {
let archetype = &*archetype;
self.trigger_on_replace(archetype, entity, [component_id].into_iter());
self.trigger_on_replace(
archetype,
entity,
[component_id].into_iter(),
#[cfg(feature = "track_location")]
Location::caller(),
);
if archetype.has_replace_observer() {
self.trigger_observers(ON_REPLACE, entity, [component_id].into_iter());
self.trigger_observers(
ON_REPLACE,
entity,
[component_id].into_iter(),
#[cfg(feature = "track_location")]
Location::caller(),
);
}
}
@ -155,9 +169,21 @@ impl<'w> DeferredWorld<'w> {
// - ON_REPLACE is able to accept ZST events
unsafe {
let archetype = &*archetype;
self.trigger_on_insert(archetype, entity, [component_id].into_iter());
self.trigger_on_insert(
archetype,
entity,
[component_id].into_iter(),
#[cfg(feature = "track_location")]
Location::caller(),
);
if archetype.has_insert_observer() {
self.trigger_observers(ON_INSERT, entity, [component_id].into_iter());
self.trigger_observers(
ON_INSERT,
entity,
[component_id].into_iter(),
#[cfg(feature = "track_location")]
Location::caller(),
);
}
}
@ -503,13 +529,22 @@ impl<'w> DeferredWorld<'w> {
archetype: &Archetype,
entity: Entity,
targets: impl Iterator<Item = ComponentId>,
#[cfg(feature = "track_location")] 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_location")]
Some(caller),
#[cfg(not(feature = "track_location"))]
None,
);
}
}
}
@ -525,13 +560,22 @@ impl<'w> DeferredWorld<'w> {
archetype: &Archetype,
entity: Entity,
targets: impl Iterator<Item = ComponentId>,
#[cfg(feature = "track_location")] 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_location")]
Some(caller),
#[cfg(not(feature = "track_location"))]
None,
);
}
}
}
@ -547,13 +591,22 @@ impl<'w> DeferredWorld<'w> {
archetype: &Archetype,
entity: Entity,
targets: impl Iterator<Item = ComponentId>,
#[cfg(feature = "track_location")] 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_location")]
Some(caller),
#[cfg(not(feature = "track_location"))]
None,
);
}
}
}
@ -569,13 +622,22 @@ impl<'w> DeferredWorld<'w> {
archetype: &Archetype,
entity: Entity,
targets: impl Iterator<Item = ComponentId>,
#[cfg(feature = "track_location")] 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_location")]
Some(caller),
#[cfg(not(feature = "track_location"))]
None,
);
}
}
}
@ -591,13 +653,22 @@ impl<'w> DeferredWorld<'w> {
archetype: &Archetype,
entity: Entity,
targets: impl Iterator<Item = ComponentId>,
#[cfg(feature = "track_location")] caller: &'static Location<'static>,
) {
if archetype.has_despawn_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_despawn {
hook(DeferredWorld { world: self.world }, entity, component_id);
hook(
DeferredWorld { world: self.world },
entity,
component_id,
#[cfg(feature = "track_location")]
Some(caller),
#[cfg(not(feature = "track_location"))]
None,
);
}
}
}
@ -613,6 +684,7 @@ impl<'w> DeferredWorld<'w> {
event: ComponentId,
target: Entity,
components: impl Iterator<Item = ComponentId> + Clone,
#[cfg(feature = "track_location")] caller: &'static Location<'static>,
) {
Observers::invoke::<_>(
self.reborrow(),
@ -621,6 +693,8 @@ impl<'w> DeferredWorld<'w> {
components,
&mut (),
&mut false,
#[cfg(feature = "track_location")]
caller,
);
}
@ -636,6 +710,7 @@ impl<'w> DeferredWorld<'w> {
components: &[ComponentId],
data: &mut E,
mut propagate: bool,
#[cfg(feature = "track_location")] caller: &'static Location<'static>,
) where
T: Traversal<E>,
{
@ -647,6 +722,8 @@ impl<'w> DeferredWorld<'w> {
components.iter().copied(),
data,
&mut propagate,
#[cfg(feature = "track_location")]
caller,
);
if !propagate {
break;

View File

@ -1594,6 +1594,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_location")]
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_location")] caller: &'static Location<'static>,
) -> &mut Self {
self.assert_not_despawned();
let change_tick = self.world.change_tick();
@ -1616,6 +1633,8 @@ impl<'w> EntityWorldMut<'w> {
self.location,
Some(component).into_iter(),
Some(storage_type).iter().cloned(),
#[cfg(feature = "track_location")]
caller,
);
self.world.flush();
self.update_location();
@ -1665,6 +1684,8 @@ impl<'w> EntityWorldMut<'w> {
self.location,
iter_components,
(*storage_types).iter().cloned(),
#[cfg(feature = "track_location")]
Location::caller(),
);
*self.world.bundles.get_storages_unchecked(bundle_id) = core::mem::take(&mut storage_types);
self.world.flush();
@ -1682,6 +1703,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<T: Bundle>(&mut self) -> Option<T> {
self.assert_not_despawned();
let world = &mut self.world;
@ -1727,6 +1749,8 @@ impl<'w> EntityWorldMut<'w> {
old_archetype,
entity,
bundle_info,
#[cfg(feature = "track_location")]
Location::caller(),
);
}
@ -1867,7 +1891,11 @@ impl<'w> EntityWorldMut<'w> {
///
/// # Safety
/// - A `BundleInfo` with the corresponding `BundleId` must have been initialized.
unsafe fn remove_bundle(&mut self, bundle: BundleId) -> EntityLocation {
unsafe fn remove_bundle(
&mut self,
bundle: BundleId,
#[cfg(feature = "track_location")] caller: &'static Location<'static>,
) -> EntityLocation {
let entity = self.entity;
let world = &mut self.world;
let location = self.location;
@ -1910,6 +1938,8 @@ impl<'w> EntityWorldMut<'w> {
old_archetype,
entity,
bundle_info,
#[cfg(feature = "track_location")]
caller,
);
}
@ -1956,14 +1986,32 @@ impl<'w> EntityWorldMut<'w> {
///
/// If the entity has been despawned while this `EntityWorldMut` is still alive.
// TODO: BundleRemover?
#[track_caller]
pub fn remove<T: Bundle>(&mut self) -> &mut Self {
self.remove_with_caller::<T>(
#[cfg(feature = "track_location")]
Location::caller(),
)
}
#[inline]
pub(crate) fn remove_with_caller<T: Bundle>(
&mut self,
#[cfg(feature = "track_location")] 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::<T>(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_location")]
caller,
)
};
self.world.flush();
self.update_location();
self
@ -1974,7 +2022,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<T: Bundle>(&mut self) -> &mut Self {
self.remove_with_requires_with_caller::<T>(
#[cfg(feature = "track_location")]
Location::caller(),
)
}
pub(crate) fn remove_with_requires_with_caller<T: Bundle>(
&mut self,
#[cfg(feature = "track_location")] caller: &'static Location<'static>,
) -> &mut Self {
self.assert_not_despawned();
let storages = &mut self.world.storages;
let components = &mut self.world.components;
@ -1983,7 +2042,13 @@ impl<'w> EntityWorldMut<'w> {
let bundle_id = bundles.register_contributed_bundle_info::<T>(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_location")]
caller,
)
};
self.world.flush();
self.update_location();
self
@ -1996,7 +2061,19 @@ impl<'w> EntityWorldMut<'w> {
/// # Panics
///
/// If the entity has been despawned while this `EntityWorldMut` is still alive.
#[track_caller]
pub fn retain<T: Bundle>(&mut self) -> &mut Self {
self.retain_with_caller::<T>(
#[cfg(feature = "track_location")]
Location::caller(),
)
}
#[inline]
pub(crate) fn retain_with_caller<T: Bundle>(
&mut self,
#[cfg(feature = "track_location")] caller: &'static Location<'static>,
) -> &mut Self {
self.assert_not_despawned();
let archetypes = &mut self.world.archetypes;
let storages = &mut self.world.storages;
@ -2016,7 +2093,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_location")]
caller,
)
};
self.world.flush();
self.update_location();
self
@ -2030,7 +2113,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_location")]
Location::caller(),
)
}
#[inline]
pub(crate) fn remove_by_id_with_caller(
&mut self,
component_id: ComponentId,
#[cfg(feature = "track_location")] caller: &'static Location<'static>,
) -> &mut Self {
self.assert_not_despawned();
let components = &mut self.world.components;
@ -2040,7 +2137,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_location")]
caller,
)
};
self.world.flush();
self.update_location();
self
@ -2054,6 +2157,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;
@ -2064,7 +2168,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_location")]
Location::caller(),
)
};
self.world.flush();
self.update_location();
@ -2076,7 +2186,19 @@ 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_location")]
Location::caller(),
)
}
#[inline]
pub(crate) fn clear_with_caller(
&mut self,
#[cfg(feature = "track_location")] caller: &'static Location<'static>,
) -> &mut Self {
self.assert_not_despawned();
let component_ids: Vec<ComponentId> = self.archetype().components().collect();
let components = &mut self.world.components;
@ -2087,7 +2209,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_location")]
caller,
)
};
self.world.flush();
self.update_location();
self
@ -2140,17 +2268,53 @@ impl<'w> EntityWorldMut<'w> {
// SAFETY: All components in the archetype exist in world
unsafe {
if archetype.has_despawn_observer() {
deferred_world.trigger_observers(ON_DESPAWN, self.entity, archetype.components());
deferred_world.trigger_observers(
ON_DESPAWN,
self.entity,
archetype.components(),
#[cfg(feature = "track_location")]
caller,
);
}
deferred_world.trigger_on_despawn(archetype, self.entity, archetype.components());
deferred_world.trigger_on_despawn(
archetype,
self.entity,
archetype.components(),
#[cfg(feature = "track_location")]
caller,
);
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_location")]
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_location")]
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_location")]
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_location")]
Location::caller(),
);
}
for component_id in archetype.components() {
@ -2387,13 +2551,29 @@ impl<'w> EntityWorldMut<'w> {
/// # Panics
///
/// If the entity has been despawned while this `EntityWorldMut` is still alive.
#[track_caller]
pub fn observe<E: Event, B: Bundle, M>(
&mut self,
observer: impl IntoObserverSystem<E, B, M>,
) -> &mut Self {
self.observe_with_caller(
observer,
#[cfg(feature = "track_location")]
Location::caller(),
)
}
pub(crate) fn observe_with_caller<E: Event, B: Bundle, M>(
&mut self,
observer: impl IntoObserverSystem<E, B, M>,
#[cfg(feature = "track_location")] caller: &'static Location<'static>,
) -> &mut Self {
self.assert_not_despawned();
self.world
.spawn(Observer::new(observer).with_entity(self.entity));
self.world.spawn_with_caller(
Observer::new(observer).with_entity(self.entity),
#[cfg(feature = "track_location")]
caller,
);
self.world.flush();
self.update_location();
self
@ -2571,19 +2751,40 @@ unsafe fn trigger_on_replace_and_on_remove_hooks_and_observers(
archetype: &Archetype,
entity: Entity,
bundle_info: &BundleInfo,
#[cfg(feature = "track_location")] caller: &'static Location<'static>,
) {
if archetype.has_replace_observer() {
deferred_world.trigger_observers(
ON_REPLACE,
entity,
bundle_info.iter_explicit_components(),
#[cfg(feature = "track_location")]
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_location")]
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_location")]
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_location")]
caller,
);
}
/// A view into a single entity and component in a world, which may either be vacant or occupied.
@ -3860,7 +4061,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<Item = OwningPtr<'a>>,
@ -3871,6 +4071,7 @@ unsafe fn insert_dynamic_bundle<
location: EntityLocation,
components: I,
storage_types: S,
#[cfg(feature = "track_location")] caller: &'static Location<'static>,
) -> EntityLocation {
struct DynamicInsertBundle<'a, I: Iterator<Item = (StorageType, OwningPtr<'a>)>> {
components: I,
@ -3896,7 +4097,7 @@ unsafe fn insert_dynamic_bundle<
bundle,
InsertMode::Replace,
#[cfg(feature = "track_location")]
Location::caller(),
caller,
)
}
}
@ -4200,7 +4401,6 @@ mod tests {
use bevy_ptr::{OwningPtr, Ptr};
use core::panic::AssertUnwindSafe;
#[cfg(feature = "track_location")]
use core::panic::Location;
#[cfg(feature = "track_location")]
use std::sync::OnceLock;
@ -5282,12 +5482,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::<TestVec>().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::<TestVec>()
.0
@ -5296,14 +5506,24 @@ mod tests {
world.commands().entity(entity).remove::<OrdB>();
}
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::<TestVec>()
.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::<TestVec>()
.0
@ -5330,7 +5550,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::<TestVec>().0.push("OrdB hook on_add");
world.commands().queue(|world: &mut World| {
world
@ -5340,21 +5565,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::<TestVec>()
.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::<TestVec>()
.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::<TestVec>()
.0
@ -5494,7 +5734,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
@ -5555,7 +5795,7 @@ mod tests {
world.register_component::<Foo>();
world
.register_component_hooks::<Foo>()
.on_add(|world, entity, _| {
.on_add(|world, entity, _, _| {
ADD_COUNT.fetch_add(1, Ordering::Relaxed);
assert_eq!(
@ -5563,7 +5803,7 @@ mod tests {
Some(&Foo(EXPECTED_VALUE.load(Ordering::Relaxed)))
);
})
.on_remove(|world, entity, _| {
.on_remove(|world, entity, _, _| {
REMOVE_COUNT.fetch_add(1, Ordering::Relaxed);
assert_eq!(
@ -5571,7 +5811,7 @@ mod tests {
Some(&Foo(EXPECTED_VALUE.load(Ordering::Relaxed)))
);
})
.on_replace(|world, entity, _| {
.on_replace(|world, entity, _, _| {
REPLACE_COUNT.fetch_add(1, Ordering::Relaxed);
assert_eq!(
@ -5579,7 +5819,7 @@ mod tests {
Some(&Foo(EXPECTED_VALUE.load(Ordering::Relaxed)))
);
})
.on_insert(|world, entity, _| {
.on_insert(|world, entity, _, _| {
INSERT_COUNT.fetch_add(1, Ordering::Relaxed);
assert_eq!(

View File

@ -1077,6 +1077,18 @@ impl World {
/// ```
#[track_caller]
pub fn spawn<B: Bundle>(&mut self, bundle: B) -> EntityWorldMut {
self.spawn_with_caller(
bundle,
#[cfg(feature = "track_location")]
Location::caller(),
)
}
pub(crate) fn spawn_with_caller<B: Bundle>(
&mut self,
bundle: B,
#[cfg(feature = "track_location")] caller: &'static Location<'static>,
) -> EntityWorldMut {
self.flush();
let change_tick = self.change_tick();
let entity = self.entities.alloc();
@ -1087,7 +1099,7 @@ impl World {
entity,
bundle,
#[cfg(feature = "track_location")]
Location::caller(),
caller,
)
};
@ -1102,7 +1114,7 @@ impl World {
#[cfg(feature = "track_location")]
self.entities
.set_spawned_or_despawned_by(entity.index(), Location::caller());
.set_spawned_or_despawned_by(entity.index(), caller);
// SAFETY: entity and location are valid, as they were just created above
unsafe { EntityWorldMut::new(self, entity, entity_location) }

View File

@ -1,5 +1,7 @@
//! Contains the [`AutoFocus`] component and related machinery.
use core::panic::Location;
use bevy_ecs::{component::ComponentId, prelude::*, world::DeferredWorld};
use crate::InputFocus;
@ -23,7 +25,12 @@ use bevy_reflect::{prelude::*, Reflect};
#[component(on_add = on_auto_focus_added)]
pub struct AutoFocus;
fn on_auto_focus_added(mut world: DeferredWorld, entity: Entity, _: ComponentId) {
fn on_auto_focus_added(
mut world: DeferredWorld,
entity: Entity,
_: ComponentId,
_: Option<&'static Location<'static>>,
) {
if let Some(mut input_focus) = world.get_resource_mut::<InputFocus>() {
input_focus.set(entity);
}

View File

@ -368,13 +368,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>,
) {
let mut input_focus = world.resource_mut::<InputFocus>();
input_focus.set(entity);
}

View File

@ -23,6 +23,7 @@
//! 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 alloc::vec::Vec;
use bevy_app::{App, Plugin, Startup};
use bevy_ecs::{

View File

@ -43,6 +43,7 @@ use bevy_window::{
WindowScaleFactorChanged,
};
use core::ops::Range;
use core::panic::Location;
use derive_more::derive::From;
use tracing::warn;
use wgpu::{BlendState, TextureFormat, TextureUsages};
@ -332,9 +333,14 @@ pub struct Camera {
pub sub_camera_view: Option<SubCameraView>,
}
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::<CameraRenderGraph>() {
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());
}
}

View File

@ -33,7 +33,7 @@ impl<C: Component> Plugin for SyncComponentPlugin<C> {
app.register_required_components::<C, SyncToRenderWorld>();
app.world_mut().register_component_hooks::<C>().on_remove(
|mut world, entity, _component_id| {
|mut world, entity, _component_id, _caller| {
let mut pending = world.resource_mut::<PendingSyncEntity>();
pending.push(EntityRecord::ComponentRemoved(entity));
},

View File

@ -2,6 +2,7 @@ mod range;
mod render_layers;
use core::any::TypeId;
use core::panic::Location;
use bevy_ecs::component::ComponentId;
use bevy_ecs::entity::hash_set::EntityHashSet;
@ -632,8 +633,12 @@ pub fn check_visibility(
/// ...
/// }
/// ```
pub fn add_visibility_class<C>(mut world: DeferredWorld<'_>, entity: Entity, _: ComponentId)
where
pub fn add_visibility_class<C>(
mut world: DeferredWorld<'_>,
entity: Entity,
_: ComponentId,
_: Option<&Location>,
) where
C: 'static,
{
if let Some(mut visibility_class) = world.get_mut::<VisibilityClass>(entity) {

View File

@ -365,7 +365,7 @@ mod tests {
let mut dst_world = World::new();
dst_world
.register_component_hooks::<A>()
.on_add(|mut world, _, _| {
.on_add(|mut world, _, _, _| {
world.commands().spawn_empty();
});
dst_world.insert_resource(reg.clone());

View File

@ -68,7 +68,7 @@ impl Plugin for ScenePlugin {
// Register component hooks for DynamicSceneRoot
app.world_mut()
.register_component_hooks::<DynamicSceneRoot>()
.on_remove(|mut world, entity, _| {
.on_remove(|mut world, entity, _, _| {
let Some(handle) = world.get::<DynamicSceneRoot>(entity) else {
return;
};
@ -87,7 +87,7 @@ impl Plugin for ScenePlugin {
// Register component hooks for SceneRoot
app.world_mut()
.register_component_hooks::<SceneRoot>()
.on_remove(|mut world, entity, _| {
.on_remove(|mut world, entity, _, _| {
if let Some(&SceneInstance(scene_instance)) = world.get::<SceneInstance>(entity) {
let Some(mut scene_spawner) = world.get_resource_mut::<SceneSpawner>() else {
return;

View File

@ -63,16 +63,22 @@ fn setup(world: &mut World) {
world
.register_component_hooks::<MyComponent>()
// 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::<MyComponent>(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::<MyComponentIndex>()
@ -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::<MyComponentIndex>());
})
// `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::<MyComponent>(entity).unwrap().0;
world.resource_mut::<MyComponentIndex>().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::<MyComponent>(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();
});

View File

@ -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::<Name>() 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::<Name>() else {
unreachable!("OnReplace hook guarantees `Name` is available on entity")
};