diff --git a/benches/benches/bevy_ecs/world/commands.rs b/benches/benches/bevy_ecs/world/commands.rs index 9f5a93bf141ad..632c9b163dfdc 100644 --- a/benches/benches/bevy_ecs/world/commands.rs +++ b/benches/benches/bevy_ecs/world/commands.rs @@ -2,8 +2,9 @@ use core::hint::black_box; use bevy_ecs::{ component::Component, - system::Commands, - world::{Command, CommandQueue, World}, + result::Result, + system::{Command, Commands}, + world::{CommandQueue, World}, }; use criterion::Criterion; @@ -136,16 +137,18 @@ struct FakeCommandA; struct FakeCommandB(u64); impl Command for FakeCommandA { - fn apply(self, world: &mut World) { + fn apply(self, world: &mut World) -> Result { black_box(self); black_box(world); + Ok(()) } } impl Command for FakeCommandB { - fn apply(self, world: &mut World) { + fn apply(self, world: &mut World) -> Result { black_box(self); black_box(world); + Ok(()) } } @@ -180,9 +183,10 @@ pub fn fake_commands(criterion: &mut Criterion) { struct SizedCommand(T); impl Command for SizedCommand { - fn apply(self, world: &mut World) { + fn apply(self, world: &mut World) -> Result { black_box(self); black_box(world); + Ok(()) } } diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 9a0403f2d9aae..aff7c1509e7fc 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -336,12 +336,12 @@ impl SparseSetIndex for BundleId { } } -// What to do on insertion if component already exists +/// What to do on insertion if a component already exists. #[derive(Clone, Copy, Eq, PartialEq)] -pub(crate) enum InsertMode { +pub enum InsertMode { /// Any existing components of a matching type will be overwritten. Replace, - /// Any existing components of a matching type will kept unchanged. + /// Any existing components of a matching type will be left unchanged. Keep, } diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index ef5db63779e1e..860916362fc99 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -73,13 +73,13 @@ pub mod prelude { IntoSystemSet, IntoSystemSetConfigs, Schedule, Schedules, SystemSet, }, system::{ - Commands, Deferred, EntityCommand, EntityCommands, In, InMut, InRef, IntoSystem, Local, - NonSend, NonSendMut, ParamSet, Populated, Query, ReadOnlySystem, Res, ResMut, Resource, - Single, System, SystemIn, SystemInput, SystemParamBuilder, SystemParamFunction, - WithParamWarnPolicy, + Command, Commands, Deferred, EntityCommand, EntityCommands, In, InMut, InRef, + IntoSystem, Local, NonSend, NonSendMut, ParamSet, Populated, Query, ReadOnlySystem, + Res, ResMut, Resource, Single, System, SystemIn, SystemInput, SystemParamBuilder, + SystemParamFunction, WithParamWarnPolicy, }, world::{ - Command, EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, + EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, FromWorld, OnAdd, OnInsert, OnRemove, OnReplace, World, }, }; diff --git a/crates/bevy_ecs/src/system/commands/command.rs b/crates/bevy_ecs/src/system/commands/command.rs new file mode 100644 index 0000000000000..eb2ba4ab77910 --- /dev/null +++ b/crates/bevy_ecs/src/system/commands/command.rs @@ -0,0 +1,345 @@ +//! This module contains the definition of the [`Command`] trait, as well as +//! blanket implementations of the trait for closures. +//! +//! It also contains functions that return closures for use with +//! [`Commands`](crate::system::Commands). + +#[cfg(feature = "track_location")] +use core::panic::Location; + +use crate::{ + bundle::{Bundle, InsertMode}, + entity::Entity, + event::{Event, Events}, + observer::TriggerTargets, + result::Result, + schedule::ScheduleLabel, + system::{CommandError, IntoSystem, Resource, SystemId, SystemInput}, + world::{FromWorld, SpawnBatchIter, World}, +}; + +/// A [`World`] mutation. +/// +/// Should be used with [`Commands::queue`](crate::system::Commands::queue). +/// +/// # Usage +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// // Our world resource +/// #[derive(Resource, Default)] +/// struct Counter(u64); +/// +/// // Our custom command +/// struct AddToCounter(u64); +/// +/// impl Command for AddToCounter { +/// fn apply(self, world: &mut World) -> Result { +/// let mut counter = world.get_resource_or_insert_with(Counter::default); +/// counter.0 += self.0; +/// Ok(()) +/// } +/// } +/// +/// fn some_system(mut commands: Commands) { +/// commands.queue(AddToCounter(42)); +/// } +/// ``` +/// +/// # Note on Generic +/// +/// The `Marker` generic is necessary to allow multiple blanket implementations +/// of `Command` for closures, like so: +/// ```ignore (This would conflict with the real implementations) +/// impl Command for FnOnce(&mut World) +/// impl Command for FnOnce(&mut World) -> Result +/// ``` +/// Without the generic, Rust would consider the two implementations to be conflicting. +/// +/// The type used for `Marker` has no connection to anything else in the implementation. +pub trait Command: Send + 'static { + /// Applies this command, causing it to mutate the provided `world`. + /// + /// This method is used to define what a command "does" when it is ultimately applied. + /// Because this method takes `self`, you can store data or settings on the type that implements this trait. + /// This data is set by the system or other source of the command, and then ultimately read in this method. + fn apply(self, world: &mut World) -> Result; + + /// Applies this command and converts any resulting error into a [`CommandError`]. + /// + /// Overwriting this method allows an implementor to return a `CommandError` directly + /// and avoid erasing the error's type. + fn apply_internal(self, world: &mut World) -> Result<(), CommandError> + where + Self: Sized, + { + match self.apply(world) { + Ok(_) => Ok(()), + Err(error) => Err(CommandError::CommandFailed(error)), + } + } + + /// Returns a new [`Command`] that, when applied, will apply the original command + /// and pass any resulting error to the provided `error_handler`. + fn with_error_handling( + self, + error_handler: Option, + ) -> impl Command + where + Self: Sized, + { + move |world: &mut World| { + if let Err(error) = self.apply_internal(world) { + // TODO: Pass the error to the global error handler if `error_handler` is `None`. + let error_handler = error_handler.unwrap_or(|_, error| panic!("{error}")); + error_handler(world, error); + } + } + } +} + +impl Command for F +where + F: FnOnce(&mut World) + Send + 'static, +{ + fn apply(self, world: &mut World) -> Result { + self(world); + Ok(()) + } +} + +impl Command for F +where + F: FnOnce(&mut World) -> Result + Send + 'static, +{ + fn apply(self, world: &mut World) -> Result { + self(world) + } +} + +/// Necessary to avoid erasing the type of the `CommandError` in +/// [`EntityCommand::with_entity`](crate::system::EntityCommand::with_entity). +impl Command<(Result, CommandError)> for F +where + F: FnOnce(&mut World) -> Result<(), CommandError> + Send + 'static, +{ + fn apply(self, world: &mut World) -> Result { + self(world)?; + Ok(()) + } + + fn apply_internal(self, world: &mut World) -> Result<(), CommandError> { + self(world) + } +} + +/// A [`Command`] that consumes an iterator of [`Bundles`](Bundle) to spawn a series of entities. +/// +/// This is more efficient than spawning the entities individually. +#[track_caller] +pub fn spawn_batch(bundles_iter: I) -> impl Command +where + I: IntoIterator + Send + Sync + 'static, + I::Item: Bundle, +{ + #[cfg(feature = "track_location")] + let caller = Location::caller(); + move |world: &mut World| { + SpawnBatchIter::new( + world, + bundles_iter.into_iter(), + #[cfg(feature = "track_location")] + caller, + ); + } +} + +/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities. +/// If any entities do not exist in the world, this command will panic. +/// +/// This is more efficient than inserting the bundles individually. +#[track_caller] +pub fn insert_batch(batch: I, mode: InsertMode) -> impl Command +where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, +{ + #[cfg(feature = "track_location")] + let caller = Location::caller(); + move |world: &mut World| { + world.insert_batch_with_caller( + batch, + mode, + #[cfg(feature = "track_location")] + caller, + ); + } +} + +/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities. +/// If any entities do not exist in the world, this command will ignore them. +/// +/// This is more efficient than inserting the bundles individually. +#[track_caller] +pub fn try_insert_batch(batch: I, mode: InsertMode) -> impl Command +where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, +{ + #[cfg(feature = "track_location")] + let caller = Location::caller(); + move |world: &mut World| { + world.try_insert_batch_with_caller( + batch, + mode, + #[cfg(feature = "track_location")] + caller, + ); + } +} + +/// A [`Command`] that inserts a [`Resource`] into the world using a value +/// created with the [`FromWorld`] trait. +#[track_caller] +pub fn init_resource() -> impl Command { + move |world: &mut World| { + world.init_resource::(); + } +} + +/// A [`Command`] that inserts a [`Resource`] into the world. +#[track_caller] +pub fn insert_resource(resource: R) -> impl Command { + #[cfg(feature = "track_location")] + let caller = Location::caller(); + move |world: &mut World| { + world.insert_resource_with_caller( + resource, + #[cfg(feature = "track_location")] + caller, + ); + } +} + +/// A [`Command`] that removes a [`Resource`] from the world. +pub fn remove_resource() -> impl Command { + move |world: &mut World| { + world.remove_resource::(); + } +} + +/// A [`Command`] that runs the system corresponding to the given [`SystemId`]. +pub fn run_system(id: SystemId<(), O>) -> impl Command { + move |world: &mut World| -> Result { + world.run_system(id)?; + Ok(()) + } +} + +/// A [`Command`] that runs the system corresponding to the given [`SystemId`] +/// and provides the given input value. +pub fn run_system_with(id: SystemId, input: I::Inner<'static>) -> impl Command +where + I: SystemInput: Send> + 'static, +{ + move |world: &mut World| -> Result { + world.run_system_with(id, input)?; + Ok(()) + } +} + +/// A [`Command`] that runs the given system, +/// caching its [`SystemId`] in a [`CachedSystemId`](crate::system::CachedSystemId) resource. +pub fn run_system_cached(system: S) -> impl Command +where + M: 'static, + S: IntoSystem<(), (), M> + Send + 'static, +{ + move |world: &mut World| -> Result { + world.run_system_cached(system)?; + Ok(()) + } +} + +/// A [`Command`] that runs the given system with the given input value, +/// caching its [`SystemId`] in a [`CachedSystemId`](crate::system::CachedSystemId) resource. +pub fn run_system_cached_with(system: S, input: I::Inner<'static>) -> impl Command +where + I: SystemInput: Send> + Send + 'static, + M: 'static, + S: IntoSystem + Send + 'static, +{ + move |world: &mut World| -> Result { + world.run_system_cached_with(system, input)?; + Ok(()) + } +} + +/// A [`Command`] that removes a system previously registered with +/// [`Commands::register_system`](crate::system::Commands::register_system) or +/// [`World::register_system`]. +pub fn unregister_system(system_id: SystemId) -> impl Command +where + I: SystemInput + Send + 'static, + O: Send + 'static, +{ + move |world: &mut World| -> Result { + world.unregister_system(system_id)?; + Ok(()) + } +} + +/// A [`Command`] that removes a system previously registered with +/// [`World::register_system_cached`]. +pub fn unregister_system_cached(system: S) -> impl Command +where + I: SystemInput + Send + 'static, + O: 'static, + M: 'static, + S: IntoSystem + Send + 'static, +{ + move |world: &mut World| -> Result { + world.unregister_system_cached(system)?; + Ok(()) + } +} + +/// A [`Command`] that runs the schedule corresponding to the given [`ScheduleLabel`]. +pub fn run_schedule(label: impl ScheduleLabel) -> impl Command { + move |world: &mut World| -> Result { + world.try_run_schedule(label)?; + Ok(()) + } +} + +/// A [`Command`] that sends a global [`Trigger`](crate::observer::Trigger) without any targets. +pub fn trigger(event: impl Event) -> impl Command { + move |world: &mut World| { + world.trigger(event); + } +} + +/// A [`Command`] that sends a [`Trigger`](crate::observer::Trigger) for the given targets. +pub fn trigger_targets( + event: impl Event, + targets: impl TriggerTargets + Send + Sync + 'static, +) -> impl Command { + move |world: &mut World| { + world.trigger_targets(event, targets); + } +} + +/// A [`Command`] that sends an arbitrary [`Event`]. +#[track_caller] +pub fn send_event(event: E) -> impl Command { + #[cfg(feature = "track_location")] + let caller = Location::caller(); + move |world: &mut World| { + let mut events = world.resource_mut::>(); + events.send_with_caller( + event, + #[cfg(feature = "track_location")] + caller, + ); + } +} diff --git a/crates/bevy_ecs/src/system/commands/entity_command.rs b/crates/bevy_ecs/src/system/commands/entity_command.rs new file mode 100644 index 0000000000000..5794fcb45631b --- /dev/null +++ b/crates/bevy_ecs/src/system/commands/entity_command.rs @@ -0,0 +1,329 @@ +//! This module contains the definition of the [`EntityCommand`] trait, as well as +//! blanket implementations of the trait for closures. +//! +//! It also contains functions that return closures for use with +//! [`EntityCommands`](crate::system::EntityCommands). + +use alloc::vec::Vec; +use log::info; + +#[cfg(feature = "track_location")] +use core::panic::Location; + +use crate::{ + bundle::{Bundle, InsertMode}, + component::{Component, ComponentId, ComponentInfo}, + entity::{Entity, EntityCloneBuilder}, + event::Event, + result::Result, + system::{Command, CommandError, IntoObserverSystem}, + world::{EntityWorldMut, FromWorld, World}, +}; +use bevy_ptr::OwningPtr; + +/// A [`Command`] which gets executed for a given [`Entity`]. +/// +/// # Examples +/// +/// ``` +/// # use std::collections::HashSet; +/// # use bevy_ecs::prelude::*; +/// use bevy_ecs::system::EntityCommand; +/// # +/// # #[derive(Component, PartialEq)] +/// # struct Name(String); +/// # impl Name { +/// # fn new(s: String) -> Self { Name(s) } +/// # fn as_str(&self) -> &str { &self.0 } +/// # } +/// +/// #[derive(Resource, Default)] +/// struct Counter(i64); +/// +/// /// A `Command` which names an entity based on a global counter. +/// fn count_name(entity: Entity, world: &mut World) { +/// // Get the current value of the counter, and increment it for next time. +/// let mut counter = world.resource_mut::(); +/// let i = counter.0; +/// counter.0 += 1; +/// +/// // Name the entity after the value of the counter. +/// world.entity_mut(entity).insert(Name::new(format!("Entity #{i}"))); +/// } +/// +/// // App creation boilerplate omitted... +/// # let mut world = World::new(); +/// # world.init_resource::(); +/// # +/// # let mut setup_schedule = Schedule::default(); +/// # setup_schedule.add_systems(setup); +/// # let mut assert_schedule = Schedule::default(); +/// # assert_schedule.add_systems(assert_names); +/// # +/// # setup_schedule.run(&mut world); +/// # assert_schedule.run(&mut world); +/// +/// fn setup(mut commands: Commands) { +/// commands.spawn_empty().queue(count_name); +/// commands.spawn_empty().queue(count_name); +/// } +/// +/// fn assert_names(named: Query<&Name>) { +/// // We use a HashSet because we do not care about the order. +/// let names: HashSet<_> = named.iter().map(Name::as_str).collect(); +/// assert_eq!(names, HashSet::from_iter(["Entity #0", "Entity #1"])); +/// } +/// ``` +/// +/// # Note on Generic +/// +/// The `Marker` generic is necessary to allow multiple blanket implementations +/// of `EntityCommand` for closures, like so: +/// ```ignore (This would conflict with the real implementations) +/// impl EntityCommand for FnOnce(Entity, &mut World) +/// impl EntityCommand for FnOnce(EntityWorldMut) +/// impl EntityCommand for FnOnce(Entity, &mut World) -> Result +/// impl EntityCommand<(World, Result)> for FnOnce(EntityWorldMut) -> Result +/// ``` +/// Without the generic, Rust would consider the implementations to be conflicting. +/// +/// The type used for `Marker` has no connection to anything else in the implementation. +pub trait EntityCommand: Send + 'static { + /// Executes this command for the given [`Entity`] and + /// returns a [`Result`] for error handling. + fn apply(self, entity: Entity, world: &mut World) -> Result; + + /// Returns a [`Command`] which executes this [`EntityCommand`] for the given [`Entity`]. + /// + /// This method is called when adding an [`EntityCommand`] to a command queue via [`Commands`](crate::system::Commands). + /// You can override the provided implementation if you can return a `Command` with a smaller memory + /// footprint than `(Entity, Self)`. + /// In most cases the provided implementation is sufficient. + #[must_use = "commands do nothing unless applied to a `World`"] + fn with_entity(self, entity: Entity) -> impl Command<(Result, CommandError)> + where + Self: Sized, + { + move |world: &mut World| -> Result<(), CommandError> { + if world.entities().contains(entity) { + match self.apply(entity, world) { + Ok(_) => Ok(()), + Err(error) => Err(CommandError::CommandFailed(error)), + } + } else { + Err(CommandError::NoSuchEntity( + entity, + world + .entities() + .entity_does_not_exist_error_details_message(entity), + )) + } + } + } +} + +impl EntityCommand for F +where + F: FnOnce(Entity, &mut World) + Send + 'static, +{ + fn apply(self, id: Entity, world: &mut World) -> Result { + self(id, world); + Ok(()) + } +} + +impl EntityCommand for F +where + F: FnOnce(Entity, &mut World) -> Result + Send + 'static, +{ + fn apply(self, id: Entity, world: &mut World) -> Result { + self(id, world) + } +} + +impl EntityCommand for F +where + F: FnOnce(EntityWorldMut) + Send + 'static, +{ + fn apply(self, id: Entity, world: &mut World) -> Result { + self(world.entity_mut(id)); + Ok(()) + } +} + +impl EntityCommand<(World, Result)> for F +where + F: FnOnce(EntityWorldMut) -> Result + Send + 'static, +{ + fn apply(self, id: Entity, world: &mut World) -> Result { + self(world.entity_mut(id)) + } +} + +/// An [`EntityCommand`] that adds the components in a [`Bundle`] to an entity, +/// replacing any that were already present. +#[track_caller] +pub fn insert(bundle: impl Bundle) -> impl EntityCommand { + #[cfg(feature = "track_location")] + let caller = Location::caller(); + move |mut entity: EntityWorldMut| { + entity.insert_with_caller( + bundle, + InsertMode::Replace, + #[cfg(feature = "track_location")] + caller, + ); + } +} + +/// An [`EntityCommand`] that adds the components in a [`Bundle`] to an entity, +/// except for any that were already present. +#[track_caller] +pub fn insert_if_new(bundle: impl Bundle) -> impl EntityCommand { + #[cfg(feature = "track_location")] + let caller = Location::caller(); + move |mut entity: EntityWorldMut| { + entity.insert_with_caller( + bundle, + InsertMode::Keep, + #[cfg(feature = "track_location")] + caller, + ); + } +} + +/// An [`EntityCommand`] that adds a dynamic component to an entity. +#[track_caller] +pub fn insert_by_id( + component_id: ComponentId, + value: T, +) -> impl EntityCommand { + 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); + }); + } +} + +/// An [`EntityCommand`] that adds a component to an entity using +/// the component's [`FromWorld`] implementation. +#[track_caller] +pub fn insert_from_world(mode: InsertMode) -> impl EntityCommand { + #[cfg(feature = "track_location")] + let caller = Location::caller(); + move |entity: Entity, world: &mut World| { + let value = T::from_world(world); + let mut entity = world.entity_mut(entity); + entity.insert_with_caller( + value, + mode, + #[cfg(feature = "track_location")] + caller, + ); + } +} + +/// An [`EntityCommand`] that removes the components in a [`Bundle`] from an entity. +pub fn remove() -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.remove::(); + } +} + +/// An [`EntityCommand`] that removes the components in a [`Bundle`] from an entity, +/// as well as the required components for each component removed. +pub fn remove_with_requires() -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.remove_with_requires::(); + } +} + +/// An [`EntityCommand`] that removes a dynamic component from an entity. +pub fn remove_by_id(component_id: ComponentId) -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.remove_by_id(component_id); + } +} + +/// An [`EntityCommand`] that removes all components from an entity. +pub fn clear() -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.clear(); + } +} + +/// An [`EntityCommand`] that removes all components from an entity, +/// except for those in the given [`Bundle`]. +pub fn retain() -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.retain::(); + } +} + +/// An [`EntityCommand`] that despawns an entity. +/// +/// # Note +/// +/// This won't clean up external references to the entity (such as parent-child relationships +/// if you're using `bevy_hierarchy`), which may leave the world in an invalid state. +pub fn despawn() -> impl EntityCommand { + #[cfg(feature = "track_location")] + let caller = Location::caller(); + move |entity: EntityWorldMut| { + entity.despawn_with_caller( + #[cfg(feature = "track_location")] + caller, + ); + } +} + +/// An [`EntityCommand`] that creates an [`Observer`](crate::observer::Observer) +/// listening for events of type `E` targeting an entity +pub fn observe( + observer: impl IntoObserverSystem, +) -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.observe(observer); + } +} + +/// An [`EntityCommand`] that clones parts of an entity onto another entity, +/// configured through [`EntityCloneBuilder`]. +pub fn clone_with( + target: Entity, + config: impl FnOnce(&mut EntityCloneBuilder) + Send + Sync + 'static, +) -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.clone_with(target, config); + } +} + +/// An [`EntityCommand`] that clones the specified components of an entity +/// and inserts them into another entity. +pub fn clone_components(target: Entity) -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.clone_components::(target); + } +} + +/// An [`EntityCommand`] that clones the specified components of an entity +/// and inserts them into another entity, then removes them from the original entity. +pub fn move_components(target: Entity) -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.move_components::(target); + } +} + +/// An [`EntityCommand`] that logs the components of an entity. +pub fn log_components() -> impl EntityCommand { + move |entity: Entity, world: &mut World| { + let debug_infos: Vec<_> = world + .inspect_entity(entity) + .map(ComponentInfo::name) + .collect(); + info!("Entity {entity}: {debug_infos:?}"); + } +} diff --git a/crates/bevy_ecs/src/system/commands/error.rs b/crates/bevy_ecs/src/system/commands/error.rs new file mode 100644 index 0000000000000..d805742cbda6a --- /dev/null +++ b/crates/bevy_ecs/src/system/commands/error.rs @@ -0,0 +1,17 @@ +//! This module contains the error type used by commands. + +use alloc::boxed::Box; +use thiserror::Error; + +use crate::entity::{Entity, EntityDoesNotExistDetails}; + +/// An error that occurs when executing a command. +#[derive(Error, Debug)] +pub enum CommandError { + /// The entity with the given ID does not exist. + #[error("Command failed because the entity with ID {0} {1}")] + NoSuchEntity(Entity, EntityDoesNotExistDetails), + /// The command returned an error. + #[error("Command returned an error: {0}")] + CommandFailed(Box), +} diff --git a/crates/bevy_ecs/src/system/commands/error_handler.rs b/crates/bevy_ecs/src/system/commands/error_handler.rs new file mode 100644 index 0000000000000..2b90dffc67a8e --- /dev/null +++ b/crates/bevy_ecs/src/system/commands/error_handler.rs @@ -0,0 +1,32 @@ +//! This module contains convenience functions that return simple error handlers +//! for use with the following methods: +//! - [`Commands::queue_fallible_with`](super::Commands::queue_fallible_with) +//! - [`Commands::override_error_handler`](super::Commands::override_error_handler) +//! - [`EntityCommands::queue_with`](super::EntityCommands::queue_with) +//! - [`EntityCommands::override_error_handler`](super::EntityCommands::override_error_handler) +//! - [`EntityEntryCommands::override_error_handler`](super::EntityEntryCommands::override_error_handler) + +use log::{error, warn}; + +use crate::{system::CommandError, world::World}; + +/// An error handler that does nothing. +pub fn silent() -> fn(&mut World, CommandError) { + |_, _| {} +} + +/// An error handler that accepts an error and logs it with [`warn!`]. +pub fn warn() -> fn(&mut World, CommandError) { + |_, error| warn!("{error}") +} + +/// An error handler that accepts an error and logs it with [`error!`]. +pub fn error() -> fn(&mut World, CommandError) { + |_, error| error!("{error}") +} + +/// An error handler that accepts an error and panics with the error in +/// the panic message. +pub fn panic() -> fn(&mut World, CommandError) { + |_, error| panic!("{error}") +} diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 43d4c465856e6..fb3c8df5db2d6 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1,30 +1,43 @@ +pub mod command; +pub mod entity_command; +pub mod error; +pub mod error_handler; + #[cfg(feature = "std")] mod parallel_scope; -use alloc::{boxed::Box, vec::Vec}; -use core::{marker::PhantomData, panic::Location}; +pub use command::Command; +pub use entity_command::EntityCommand; +pub use error::CommandError; + +#[cfg(feature = "std")] +pub use parallel_scope::*; + +use alloc::boxed::Box; +use core::marker::PhantomData; +use log::error; + +#[cfg(feature = "track_location")] +use core::panic::Location; -use super::{Deferred, IntoObserverSystem, IntoSystem, RegisteredSystem, Resource}; use crate::{ self as bevy_ecs, bundle::{Bundle, InsertMode}, change_detection::Mut, - component::{Component, ComponentId, ComponentInfo, Mutable}, + component::{Component, ComponentId, Mutable}, entity::{Entities, Entity, EntityCloneBuilder}, - event::{Event, Events}, + event::Event, observer::{Observer, TriggerTargets}, schedule::ScheduleLabel, - system::{input::SystemInput, SystemId}, + system::{ + input::SystemInput, Deferred, IntoObserverSystem, IntoSystem, RegisteredSystem, Resource, + SystemId, + }, world::{ - command_queue::RawCommandQueue, unsafe_world_cell::UnsafeWorldCell, Command, CommandQueue, - EntityWorldMut, FromWorld, SpawnBatchIter, World, + command_queue::RawCommandQueue, unsafe_world_cell::UnsafeWorldCell, CommandQueue, + EntityWorldMut, FromWorld, World, }, }; -use bevy_ptr::OwningPtr; -use log::{error, info, warn}; - -#[cfg(feature = "std")] -pub use parallel_scope::*; /// A [`Command`] queue to perform structural changes to the [`World`]. /// @@ -77,10 +90,39 @@ pub use parallel_scope::*; /// # } /// ``` /// +/// # Error handling +/// +/// Commands return a [`Result`](crate::result::Result), which gets passed to +/// an error handler. Error handlers are functions/closures of the form +/// `fn(&mut World, CommandError)`. +/// +/// By default, if a command returns an error, it will be passed to the +/// global error handler. Currently, the global error handler just panics; +/// in the future, this will be configurable. +/// +/// [`Commands::override_error_handler`] allows you to override a [`Commands`] +/// instance's default error handler. This method takes an error handler +/// which will be used by all subsequent commands queued through either +/// [`Commands::queue_fallible`] or built-in commands' dedicated methods. +/// +/// [`Commands::queue_fallible_with`] allows you to provide an error handler +/// directly to a command. This is unaffected by any default or override. +/// If you would like to use this method with built-in commands, +/// the [`command`] module provides unqueued forms of built-in commands +/// that you can queue manually. +/// +/// The [`error_handler`] module provides some simple error handlers for convenience. +/// /// [`ApplyDeferred`]: crate::schedule::ApplyDeferred pub struct Commands<'w, 's> { queue: InternalQueue<'s>, entities: &'w Entities, + /// This can be set using [`Commands::override_error_handler`] to override + /// the global error handler for all subsequent commands, which would be + /// more convenient than using [`Commands::queue_fallible_with`] to override + /// each command individually if you wanted to use the same error handler for + /// all of them. + error_handler_override: Option, } // SAFETY: All commands [`Command`] implement [`Send`] @@ -176,6 +218,7 @@ const _: () = { Commands { queue: InternalQueue::CommandQueue(f0), entities: f1, + error_handler_override: None, } } } @@ -212,6 +255,7 @@ impl<'w, 's> Commands<'w, 's> { Self { queue: InternalQueue::CommandQueue(Deferred(queue)), entities, + error_handler_override: None, } } @@ -229,6 +273,7 @@ impl<'w, 's> Commands<'w, 's> { Self { queue: InternalQueue::RawCommandQueue(queue), entities, + error_handler_override: None, } } @@ -259,6 +304,7 @@ impl<'w, 's> Commands<'w, 's> { } }, entities: self.entities, + error_handler_override: self.error_handler_override, } } @@ -377,6 +423,9 @@ impl<'w, 's> Commands<'w, 's> { /// Returns the [`EntityCommands`] for the requested [`Entity`]. /// + /// This method does not guarantee that commands queued by the `EntityCommands` + /// will be successful, since the entity could be despawned before they are executed. + /// /// # Panics /// /// This method panics if the requested entity does not exist. @@ -436,8 +485,8 @@ impl<'w, 's> Commands<'w, 's> { /// /// Returns `None` if the entity does not exist. /// - /// This method does not guarantee that `EntityCommands` will be successfully applied, - /// since another command in the queue may delete the entity before them. + /// This method does not guarantee that commands queued by the `EntityCommands` + /// will be successful, since the entity could be despawned before they are executed. /// /// # Example /// @@ -515,35 +564,36 @@ impl<'w, 's> Commands<'w, 's> { I: IntoIterator + Send + Sync + 'static, I::Item: Bundle, { - #[cfg(feature = "track_location")] - let caller = Location::caller(); - self.queue(move |world: &mut World| { - SpawnBatchIter::new( - world, - bundles_iter.into_iter(), - #[cfg(feature = "track_location")] - caller, - ); - }); + self.queue(command::spawn_batch(bundles_iter)); } /// Pushes a generic [`Command`] to the command queue. /// - /// `command` can be a built-in command, custom struct that implements [`Command`] or a closure - /// that takes [`&mut World`](World) as an argument. + /// The command can be: + /// - A custom struct that implements [`Command`]. + /// - A closure or function that matches one of the following signatures: + /// - [`(&mut World)`](World) + /// - [`(&mut World)`](World)`->`[`Result`](crate::result::Result) + /// - A built-in command from the [`command`] module. + /// + /// Commands can return a [`Result`], but this method will ignore them. + /// If you want to queue a command with error handling, + /// use [`Commands::queue_fallible`] or [`Commands::queue_fallible_with`]. + /// /// # Example /// /// ``` - /// # use bevy_ecs::{world::Command, prelude::*}; + /// # use bevy_ecs::prelude::*; /// #[derive(Resource, Default)] /// struct Counter(u64); /// /// struct AddToCounter(u64); /// /// impl Command for AddToCounter { - /// fn apply(self, world: &mut World) { + /// fn apply(self, world: &mut World) -> Result { /// let mut counter = world.get_resource_or_insert_with(Counter::default); /// counter.0 += self.0; + /// Ok(()) /// } /// } /// @@ -559,7 +609,7 @@ impl<'w, 's> Commands<'w, 's> { /// # bevy_ecs::system::assert_is_system(add_three_to_counter_system); /// # bevy_ecs::system::assert_is_system(add_twenty_five_to_counter_system); /// ``` - pub fn queue(&mut self, command: C) { + pub fn queue, M: 'static>(&mut self, command: C) { match &mut self.queue { InternalQueue::CommandQueue(queue) => { queue.push(command); @@ -574,6 +624,75 @@ impl<'w, 's> Commands<'w, 's> { } } + /// Pushes a generic [`Command`] to the command queue with error handling. + /// + /// The command can be: + /// - A custom struct that implements [`Command`]. + /// - A closure or function that has the signature [`(&mut World)`](World)`->`[`Result`](crate::result::Result). + /// - A built-in command from the [`command`] module. + /// + /// If the command returns an error, it will panic by default. You can use + /// [`Commands::queue_fallible_with`] to override an individual command's error handler, + /// or you can use [`Commands::override_error_handler`] to override the default + /// error handler for all subsequent commands queued by this [`Commands`] instance. + pub fn queue_fallible, M: 'static>(&mut self, command: C) { + self.queue(command.with_error_handling(self.error_handler_override)); + } + + /// Pushes a generic [`Command`] to the command queue with a particular error handler. + /// + /// The command can be: + /// - A custom struct that implements [`Command`]. + /// - A closure or function that has the signature [`(&mut World)`](World)`->`[`Result`](crate::result::Result). + /// - A built-in command from the [`command`] module. + /// + /// If the command returns an error, it will be passed to `error_handler`. + /// + /// See the [`command`] module for built-in fallible commands that can be + /// queued manually, as well as the [`error_handler`] module for simple + /// error handlers. + pub fn queue_fallible_with, M: 'static>( + &mut self, + command: C, + error_handler: fn(&mut World, CommandError), + ) { + self.queue(command.with_error_handling(Some(error_handler))); + } + + /// Pushes a generic [`Command`] to the command queue with error handling. + /// + /// If the command returns an error, it will be passed to the [`Commands`] instance's + /// error handler override if set, or `error_handler` otherwise. + // TODO: This is only useful for commands that fail differently (non-panic) by default, but + // still want to be overridden by the Commands instance's setting. It can be removed once + // all commands obey the global error handler by default. + fn queue_fallible_with_default, M: 'static>( + &mut self, + command: C, + error_handler: fn(&mut World, CommandError), + ) { + let error_handler = self.error_handler_override.unwrap_or(error_handler); + self.queue(command.with_error_handling(Some(error_handler))); + } + + /// Sets the [`Commands`] instance to use a custom error handler when encountering an error. + /// + /// This will apply to all subsequent commands. You can use [`Self::reset_error_handler`] to undo this. + /// + /// `fn()` can be a closure if it doesn't capture its environment. + pub fn override_error_handler(&mut self, error_handler: fn(&mut World, CommandError)) { + self.error_handler_override = Some(error_handler); + } + + /// Resets the [`Commands`] instance's error handler, allowing commands + /// to respond to errors in their default manner. + /// + /// This is only useful if the instance's error handler was previously overridden + /// by [`Self::override_error_handler`]. + pub fn reset_error_handler(&mut self) { + self.error_handler_override = None; + } + /// Pushes a [`Command`] to the queue for creating entities, if needed, /// and for adding a bundle to each entity. /// @@ -642,7 +761,7 @@ impl<'w, 's> Commands<'w, 's> { I: IntoIterator + Send + Sync + 'static, B: Bundle, { - self.queue(insert_batch(batch, InsertMode::Replace)); + self.queue(command::insert_batch(batch, InsertMode::Replace)); } /// Pushes a [`Command`] to the queue for adding a [`Bundle`] type to a batch of [`Entities`](Entity). @@ -669,7 +788,7 @@ impl<'w, 's> Commands<'w, 's> { I: IntoIterator + Send + Sync + 'static, B: Bundle, { - self.queue(insert_batch(batch, InsertMode::Keep)); + self.queue(command::insert_batch(batch, InsertMode::Keep)); } /// Pushes a [`Command`] to the queue for adding a [`Bundle`] type to a batch of [`Entities`](Entity). @@ -694,7 +813,7 @@ impl<'w, 's> Commands<'w, 's> { I: IntoIterator + Send + Sync + 'static, B: Bundle, { - self.queue(try_insert_batch(batch, InsertMode::Replace)); + self.queue(command::try_insert_batch(batch, InsertMode::Replace)); } /// Pushes a [`Command`] to the queue for adding a [`Bundle`] type to a batch of [`Entities`](Entity). @@ -719,7 +838,7 @@ impl<'w, 's> Commands<'w, 's> { I: IntoIterator + Send + Sync + 'static, B: Bundle, { - self.queue(try_insert_batch(batch, InsertMode::Keep)); + self.queue(command::try_insert_batch(batch, InsertMode::Keep)); } /// Pushes a [`Command`] to the queue for inserting a [`Resource`] in the [`World`] with an inferred value. @@ -748,9 +867,7 @@ impl<'w, 's> Commands<'w, 's> { /// ``` #[track_caller] pub fn init_resource(&mut self) { - self.queue(move |world: &mut World| { - world.init_resource::(); - }); + self.queue(command::init_resource::()); } /// Pushes a [`Command`] to the queue for inserting a [`Resource`] in the [`World`] with a specific value. @@ -780,15 +897,7 @@ impl<'w, 's> Commands<'w, 's> { /// ``` #[track_caller] pub fn insert_resource(&mut self, resource: R) { - #[cfg(feature = "track_location")] - let caller = Location::caller(); - self.queue(move |world: &mut World| { - world.insert_resource_with_caller( - resource, - #[cfg(feature = "track_location")] - caller, - ); - }); + self.queue(command::insert_resource(resource)); } /// Pushes a [`Command`] to the queue for removing a [`Resource`] from the [`World`]. @@ -812,9 +921,7 @@ impl<'w, 's> Commands<'w, 's> { /// # bevy_ecs::system::assert_is_system(system); /// ``` pub fn remove_resource(&mut self) { - self.queue(move |world: &mut World| { - world.remove_resource::(); - }); + self.queue(command::remove_resource::()); } /// Runs the system corresponding to the given [`SystemId`]. @@ -827,7 +934,7 @@ impl<'w, 's> Commands<'w, 's> { /// execution of the system happens later. To get the output of a system, use /// [`World::run_system`] or [`World::run_system_with`] instead of running the system as a command. pub fn run_system(&mut self, id: SystemId) { - self.run_system_with(id, ()); + self.queue_fallible_with_default(command::run_system(id), error_handler::warn()); } /// Runs the system corresponding to the given [`SystemId`]. @@ -843,11 +950,10 @@ impl<'w, 's> Commands<'w, 's> { where I: SystemInput: Send> + 'static, { - self.queue(move |world: &mut World| { - if let Err(error) = world.run_system_with(id, input) { - warn!("{error}"); - } - }); + self.queue_fallible_with_default( + command::run_system_with(id, input), + error_handler::warn(), + ); } /// Registers a system and returns a [`SystemId`] so it can later be called by [`World::run_system`]. @@ -908,12 +1014,8 @@ impl<'w, 's> Commands<'w, 's> { O: Send + 'static, { let entity = self.spawn_empty().id(); - let system = RegisteredSystem::new(Box::new(IntoSystem::into_system(system))); - self.queue(move |world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.insert(system); - } - }); + let system = RegisteredSystem::::new(Box::new(IntoSystem::into_system(system))); + self.entity(entity).insert(system); SystemId::from_entity(entity) } @@ -925,11 +1027,10 @@ impl<'w, 's> Commands<'w, 's> { I: SystemInput + Send + 'static, O: Send + 'static, { - self.queue(move |world: &mut World| { - if let Err(error) = world.unregister_system(system_id) { - warn!("{error}"); - } - }); + self.queue_fallible_with_default( + command::unregister_system(system_id), + error_handler::warn(), + ); } /// Removes a system previously registered with [`World::register_system_cached`]. @@ -944,11 +1045,10 @@ impl<'w, 's> Commands<'w, 's> { &mut self, system: S, ) { - self.queue(move |world: &mut World| { - if let Err(error) = world.unregister_system_cached(system) { - warn!("{error}"); - } - }); + self.queue_fallible_with_default( + command::unregister_system_cached(system), + error_handler::warn(), + ); } /// Similar to [`Self::run_system`], but caching the [`SystemId`] in a @@ -959,7 +1059,7 @@ impl<'w, 's> Commands<'w, 's> { &mut self, system: S, ) { - self.run_system_cached_with(system, ()); + self.queue_fallible_with_default(command::run_system_cached(system), error_handler::warn()); } /// Similar to [`Self::run_system_with`], but caching the [`SystemId`] in a @@ -972,11 +1072,10 @@ impl<'w, 's> Commands<'w, 's> { M: 'static, S: IntoSystem + Send + 'static, { - self.queue(move |world: &mut World| { - if let Err(error) = world.run_system_cached_with(system, input) { - warn!("{error}"); - } - }); + self.queue_fallible_with_default( + command::run_system_cached_with(system, input), + error_handler::warn(), + ); } /// Sends a "global" [`Trigger`] without any targets. This will run any [`Observer`] of the `event` that @@ -984,9 +1083,7 @@ impl<'w, 's> Commands<'w, 's> { /// /// [`Trigger`]: crate::observer::Trigger pub fn trigger(&mut self, event: impl Event) { - self.queue(move |world: &mut World| { - world.trigger(event); - }); + self.queue(command::trigger(event)); } /// Sends a [`Trigger`] for the given targets. This will run any [`Observer`] of the `event` that @@ -998,9 +1095,7 @@ impl<'w, 's> Commands<'w, 's> { event: impl Event, targets: impl TriggerTargets + Send + Sync + 'static, ) { - self.queue(move |world: &mut World| { - world.trigger_targets(event, targets); - }); + self.queue(command::trigger_targets(event, targets)); } /// Spawns an [`Observer`] and returns the [`EntityCommands`] associated @@ -1028,16 +1123,7 @@ impl<'w, 's> Commands<'w, 's> { /// [`EventWriter`]: crate::event::EventWriter #[track_caller] pub fn send_event(&mut self, event: E) -> &mut Self { - #[cfg(feature = "track_location")] - let caller = Location::caller(); - self.queue(move |world: &mut World| { - let mut events = world.resource_mut::>(); - events.send_with_caller( - event, - #[cfg(feature = "track_location")] - caller, - ); - }); + self.queue(command::send_event(event)); self } @@ -1081,87 +1167,48 @@ impl<'w, 's> Commands<'w, 's> { /// # assert_eq!(world.resource::().0, 1); /// ``` pub fn run_schedule(&mut self, label: impl ScheduleLabel) { - self.queue(|world: &mut World| { - if let Err(error) = world.try_run_schedule(label) { - panic!("Failed to run schedule: {error}"); - } - }); + self.queue_fallible_with_default(command::run_schedule(label), error_handler::warn()); } } -/// A [`Command`] which gets executed for a given [`Entity`]. +/// A list of commands that will be run to modify an [`Entity`]. /// -/// # Examples +/// # Note /// -/// ``` -/// # use std::collections::HashSet; -/// # use bevy_ecs::prelude::*; -/// use bevy_ecs::system::EntityCommand; -/// # -/// # #[derive(Component, PartialEq)] -/// # struct Name(String); -/// # impl Name { -/// # fn new(s: String) -> Self { Name(s) } -/// # fn as_str(&self) -> &str { &self.0 } -/// # } +/// Most [`Commands`] (and thereby [`EntityCommands`]) are deferred: when you call the command, +/// if it requires mutable access to the [`World`] (that is, if it removes, adds, or changes something), +/// it's not executed immediately. Instead, the command is added to a "command queue." +/// The command queue is applied between [`Schedules`](bevy_ecs::schedule::Schedule), one by one, +/// so that each command can have exclusive access to the World. /// -/// #[derive(Resource, Default)] -/// struct Counter(i64); +/// # Fallible /// -/// /// A `Command` which names an entity based on a global counter. -/// fn count_name(entity: Entity, world: &mut World) { -/// // Get the current value of the counter, and increment it for next time. -/// let mut counter = world.resource_mut::(); -/// let i = counter.0; -/// counter.0 += 1; +/// Due to their deferred nature, an entity you're trying to change with an [`EntityCommand`] can be +/// despawned by the time the command is executed. All deferred entity commands will check if the +/// entity exists at the time of execution and will return an error if it doesn't. /// -/// // Name the entity after the value of the counter. -/// world.entity_mut(entity).insert(Name::new(format!("Entity #{i}"))); -/// } +/// # Error handling /// -/// // App creation boilerplate omitted... -/// # let mut world = World::new(); -/// # world.init_resource::(); -/// # -/// # let mut setup_schedule = Schedule::default(); -/// # setup_schedule.add_systems(setup); -/// # let mut assert_schedule = Schedule::default(); -/// # assert_schedule.add_systems(assert_names); -/// # -/// # setup_schedule.run(&mut world); -/// # assert_schedule.run(&mut world); +/// Entity commands return a [`Result`](crate::result::Result), which gets passed to +/// an error handler. Error handlers are functions/closures of the form +/// `fn(&mut World, CommandError)`. /// -/// fn setup(mut commands: Commands) { -/// commands.spawn_empty().queue(count_name); -/// commands.spawn_empty().queue(count_name); -/// } +/// By default, if a command returns an error, it will be passed to the +/// global error handler. Currently, the global error handler just panics; +/// in the future, this will be configurable. /// -/// fn assert_names(named: Query<&Name>) { -/// // We use a HashSet because we do not care about the order. -/// let names: HashSet<_> = named.iter().map(Name::as_str).collect(); -/// assert_eq!(names, HashSet::from_iter(["Entity #0", "Entity #1"])); -/// } -/// ``` -pub trait EntityCommand: Send + 'static { - /// Executes this command for the given [`Entity`]. - fn apply(self, entity: Entity, world: &mut World); - - /// Returns a [`Command`] which executes this [`EntityCommand`] for the given [`Entity`]. - /// - /// This method is called when adding an [`EntityCommand`] to a command queue via [`Commands`]. - /// You can override the provided implementation if you can return a `Command` with a smaller memory - /// footprint than `(Entity, Self)`. - /// In most cases the provided implementation is sufficient. - #[must_use = "commands do nothing unless applied to a `World`"] - fn with_entity(self, entity: Entity) -> impl Command - where - Self: Sized, - { - move |world: &mut World| self.apply(entity, world) - } -} - -/// A list of commands that will be run to modify an [entity](crate::entity). +/// [`EntityCommands::override_error_handler`] allows you to override an [`EntityCommands`] +/// instance's default error handler. This method takes an error handler +/// which will be used by all subsequent commands queued through either +/// [`EntityCommands::queue`] or built-in commands' dedicated methods. +/// +/// [`EntityCommands::queue_with`] allows you to provide an error handler +/// directly to a command. This is unaffected by any default or override. +/// If you would like to use this method with built-in commands, +/// the [`entity_command`] module provides unqueued forms of built-in entity commands +/// that you can queue manually. +/// +/// The [`error_handler`] module provides some simple error handlers for convenience. pub struct EntityCommands<'a> { pub(crate) entity: Entity, pub(crate) commands: Commands<'a, 'a>, @@ -1281,7 +1328,7 @@ impl<'a> EntityCommands<'a> { /// ``` #[track_caller] pub fn insert(&mut self, bundle: impl Bundle) -> &mut Self { - self.queue(insert(bundle, InsertMode::Replace)) + self.queue(entity_command::insert(bundle)) } /// Similar to [`Self::insert`] but will only insert if the predicate returns true. @@ -1341,7 +1388,7 @@ impl<'a> EntityCommands<'a> { /// To avoid a panic in this case, use the command [`Self::try_insert_if_new`] instead. #[track_caller] pub fn insert_if_new(&mut self, bundle: impl Bundle) -> &mut Self { - self.queue(insert(bundle, InsertMode::Keep)) + self.queue(entity_command::insert_if_new(bundle)) } /// Adds a [`Bundle`] of components to the entity without overwriting if the @@ -1390,19 +1437,7 @@ impl<'a> EntityCommands<'a> { component_id: ComponentId, value: T, ) -> &mut Self { - let caller = Location::caller(); - self.queue(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); - }); - } else { - panic!("error[B0003]: {caller}: Could not insert a component {component_id:?} (with type {}) for entity {entity}, which {}. See: https://bevyengine.org/learn/errors/b0003", core::any::type_name::(), world.entities().entity_does_not_exist_error_details_message(entity)); - } - }) + self.queue(entity_command::insert_by_id(component_id, value)) } /// Attempts to add a dynamic component to an entity. @@ -1419,16 +1454,10 @@ impl<'a> EntityCommands<'a> { component_id: ComponentId, value: T, ) -> &mut Self { - self.queue(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); - }); - } - }) + self.queue_with_default( + entity_command::insert_by_id(component_id, value), + error_handler::silent(), + ) } /// Tries to add a [`Bundle`] of components to the entity. @@ -1481,7 +1510,7 @@ impl<'a> EntityCommands<'a> { /// ``` #[track_caller] pub fn try_insert(&mut self, bundle: impl Bundle) -> &mut Self { - self.queue(try_insert(bundle, InsertMode::Replace)) + self.queue_with_default(entity_command::insert(bundle), error_handler::silent()) } /// Similar to [`Self::try_insert`] but will only try to insert if the predicate returns true. @@ -1580,7 +1609,10 @@ impl<'a> EntityCommands<'a> { /// 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)) + self.queue_with_default( + entity_command::insert_if_new(bundle), + error_handler::silent(), + ) } /// Removes a [`Bundle`] of components from the entity. @@ -1622,11 +1654,7 @@ impl<'a> EntityCommands<'a> { where T: Bundle, { - self.queue(move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.remove::(); - } - }) + self.queue_with_default(entity_command::remove::(), error_handler::silent()) } /// Removes all components in the [`Bundle`] components and remove all required components for each component in the [`Bundle`] from entity. @@ -1654,11 +1682,10 @@ impl<'a> EntityCommands<'a> { /// # bevy_ecs::system::assert_is_system(remove_with_requires_system); /// ``` pub fn remove_with_requires(&mut self) -> &mut Self { - self.queue(move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.remove_with_requires::(); - } - }) + self.queue_with_default( + entity_command::remove_with_requires::(), + error_handler::silent(), + ) } /// Removes a dynamic [`Component`] from the entity if it exists. @@ -1667,23 +1694,19 @@ impl<'a> EntityCommands<'a> { /// /// Panics if the provided [`ComponentId`] does not exist in the [`World`]. pub fn remove_by_id(&mut self, component_id: ComponentId) -> &mut Self { - self.queue(move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.remove_by_id(component_id); - } - }) + self.queue_with_default( + entity_command::remove_by_id(component_id), + error_handler::silent(), + ) } /// Removes all components associated with the entity. pub fn clear(&mut self) -> &mut Self { - self.queue(move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.clear(); - } - }) + self.queue_with_default(entity_command::clear(), error_handler::silent()) } /// Despawns the entity. + /// /// This will emit a warning if the entity does not exist. /// /// See [`World::despawn`] for more details. @@ -1712,19 +1735,38 @@ impl<'a> EntityCommands<'a> { /// ``` #[track_caller] pub fn despawn(&mut self) { - self.queue(despawn(true)); + self.queue_with_default(entity_command::despawn(), error_handler::warn()); } /// Despawns the entity. + /// /// This will not emit a warning if the entity does not exist, essentially performing /// the same function as [`Self::despawn`] without emitting warnings. #[track_caller] pub fn try_despawn(&mut self) { - self.queue(despawn(false)); + self.queue_with_default(entity_command::despawn(), error_handler::silent()); } /// Pushes an [`EntityCommand`] to the queue, which will get executed for the current [`Entity`]. /// + /// The command can be: + /// - A custom struct that implements [`EntityCommand`]. + /// - A closure or function that matches one of the following signatures: + /// - `(Entity, &mut World)` + /// - `(Entity, &mut World) ->`[`Result`](crate::result::Result) + /// - [`(EntityWorldMut)`](EntityWorldMut) + /// - [`(EntityWorldMut)`](EntityWorldMut)`->`[`Result`](crate::result::Result) + /// - A built-in command from the [`entity_command`] module. + /// + /// All entity commands are fallible, because they must return an error if the entity + /// doesn't exist when the command is executed. Therefore, all entity commands are + /// queued with error handling. + /// + /// If the command returns an error, it will panic by default. You can use + /// [`EntityCommands::queue_with`] to override an individual command's error handler, + /// or you can use [`EntityCommands::override_error_handler`] to override the default + /// error handler for all subsequent commands queued by this [`EntityCommands`] instance. + /// /// # Examples /// /// ``` @@ -1740,7 +1782,82 @@ impl<'a> EntityCommands<'a> { /// # bevy_ecs::system::assert_is_system(my_system); /// ``` pub fn queue(&mut self, command: impl EntityCommand) -> &mut Self { - self.commands.queue(command.with_entity(self.entity)); + self.commands + .queue_fallible(command.with_entity(self.entity)); + self + } + + /// Pushes an [`EntityCommand`] to the queue, which will get executed for the current [`Entity`]. + /// + /// The command can be: + /// - A custom struct that implements [`EntityCommand`]. + /// - A closure or function that matches one of the following signatures: + /// - `(Entity, &mut World)` + /// - `(Entity, &mut World) ->`[`Result`](crate::result::Result) + /// - [`(EntityWorldMut)`](EntityWorldMut) + /// - [`(EntityWorldMut)`](EntityWorldMut)`->`[`Result`](crate::result::Result) + /// - A built-in command from the [`entity_command`] module. + /// + /// All entity commands are fallible, because they must return an error if the entity + /// doesn't exist when the command is executed. Therefore, all entity commands are + /// queued with error handling. + /// + /// If the command returns an error, it will be passed to `error_handler`. + /// + /// See the [`entity_command`] module for built-in entity commands that can be + /// queued manually, as well as the [`error_handler`] module for simple + /// error handlers. + pub fn queue_with( + &mut self, + command: impl EntityCommand, + error_handler: fn(&mut World, CommandError), + ) -> &mut Self { + self.commands + .queue_fallible_with(command.with_entity(self.entity), error_handler); + self + } + + /// Pushes an [`EntityCommand`] to the queue, which will get executed for the current [`Entity`]. + /// + /// If the command returns an error, it will be passed to the [`EntityCommands`] instance's + /// error handler override if set, or `default_error_handler` otherwise. + // TODO: This is only useful for commands that fail differently (non-panic) by default, but + // still want to be overridden by the EntityCommands instance's setting. It can be removed once + // all commands obey the global error handler by default. + fn queue_with_default( + &mut self, + command: impl EntityCommand, + default_error_handler: fn(&mut World, CommandError), + ) -> &mut Self { + let error_handler = self + .commands + .error_handler_override + .unwrap_or(default_error_handler); + self.commands + .queue_fallible_with(command.with_entity(self.entity), error_handler); + self + } + + /// Sets the [`EntityCommands`] instance to use a custom error handler when encountering an error. + /// + /// This will apply to all subsequent commands. You can use [`Self::reset_error_handler`] to undo this. + /// + /// `fn()` can be a closure if it doesn't capture its environment. + pub fn override_error_handler( + &mut self, + error_handler: fn(&mut World, CommandError), + ) -> &mut Self { + self.commands.override_error_handler(error_handler); + self + } + + /// Resets the [`EntityCommands`] instance's error handler, allowing commands + /// to respond to errors in their default manner. + /// + /// This is only useful if the instance's error handler was previously overridden + /// by [`Self::override_error_handler`]. + pub fn reset_error_handler(&mut self) -> &mut Self { + self.commands.reset_error_handler(); self } @@ -1785,11 +1902,7 @@ impl<'a> EntityCommands<'a> { where T: Bundle, { - self.queue(move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.retain::(); - } - }) + self.queue_with_default(entity_command::retain::(), error_handler::panic()) } /// Logs the components of the entity at the info level. @@ -1798,13 +1911,7 @@ impl<'a> EntityCommands<'a> { /// /// The command will panic when applied if the associated entity does not exist. pub fn log_components(&mut self) -> &mut Self { - self.queue(move |entity: Entity, world: &mut World| { - let debug_infos: Vec<_> = world - .inspect_entity(entity) - .map(ComponentInfo::name) - .collect(); - info!("Entity {entity}: {debug_infos:?}"); - }) + self.queue(entity_command::log_components()) } /// Returns the underlying [`Commands`]. @@ -1826,16 +1933,12 @@ impl<'a> EntityCommands<'a> { self } - /// Creates an [`Observer`] listening for a trigger of type `T` that targets this entity. + /// Creates an [`Observer`] listening for events of type `E` targeting this entity. pub fn observe( &mut self, observer: impl IntoObserverSystem, ) -> &mut Self { - self.queue(move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.observe(observer); - } - }) + self.queue_with_default(entity_command::observe(observer), error_handler::silent()) } /// Clones parts of an entity (components, observers, etc.) onto another entity, @@ -1883,11 +1986,10 @@ impl<'a> EntityCommands<'a> { target: Entity, config: impl FnOnce(&mut EntityCloneBuilder) + Send + Sync + 'static, ) -> &mut Self { - self.queue(move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.clone_with(target, config); - } - }) + self.queue_with_default( + entity_command::clone_with(target, config), + error_handler::silent(), + ) } /// Spawns a clone of this entity and returns the [`EntityCommands`] of the clone. @@ -1983,11 +2085,10 @@ impl<'a> EntityCommands<'a> { /// /// The command will panic when applied if the target entity does not exist. pub fn clone_components(&mut self, target: Entity) -> &mut Self { - self.queue(move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.clone_components::(target); - } - }) + self.queue_with_default( + entity_command::clone_components::(target), + error_handler::silent(), + ) } /// Clones the specified components of this entity and inserts them into another entity, @@ -2000,11 +2101,10 @@ impl<'a> EntityCommands<'a> { /// /// The command will panic when applied if the target entity does not exist. pub fn move_components(&mut self, target: Entity) -> &mut Self { - self.queue(move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.move_components::(target); - } - }) + self.queue_with_default( + entity_command::move_components::(target), + error_handler::silent(), + ) } } @@ -2028,6 +2128,29 @@ impl<'a, T: Component> EntityEntryCommands<'a, T> { } impl<'a, T: Component> EntityEntryCommands<'a, T> { + /// Sets the [`EntityEntryCommands`] instance to use a custom error handler when encountering an error. + /// + /// This will apply to all subsequent commands. You can use [`Self::reset_error_handler`] to undo this. + /// + /// `fn()` can be a closure if it doesn't capture its environment. + pub fn override_error_handler( + &mut self, + error_handler: fn(&mut World, CommandError), + ) -> &mut Self { + self.entity_commands.override_error_handler(error_handler); + self + } + + /// Resets the [`EntityEntryCommands`] instance's error handler, allowing commands + /// to respond to errors in their default manner. + /// + /// This is only useful if the instance's error handler was previously overridden + /// by [`Self::override_error_handler`]. + pub fn reset_error_handler(&mut self) -> &mut Self { + self.entity_commands.reset_error_handler(); + self + } + /// [Insert](EntityCommands::insert) `default` into this entity, if `T` is not already present. /// /// See also [`or_insert_with`](Self::or_insert_with). @@ -2103,146 +2226,12 @@ impl<'a, T: Component> EntityEntryCommands<'a, T> { where T: FromWorld, { - let caller = Location::caller(); - self.entity_commands.queue(move |entity: Entity, world: &mut World| { - let value = T::from_world(world); - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.insert_with_caller( - value, - InsertMode::Keep, - #[cfg(feature = "track_location")] - caller, - ); - } else { - panic!("error[B0003]: {caller}: Could not insert a bundle (of type `{}`) for {entity}, which {}. See: https://bevyengine.org/learn/errors/b0003", core::any::type_name::(), world.entities().entity_does_not_exist_error_details_message(entity) ); - } - }); + self.entity_commands + .queue(entity_command::insert_from_world::(InsertMode::Keep)); self } } -impl Command for F -where - F: FnOnce(&mut World) + Send + 'static, -{ - fn apply(self, world: &mut World) { - self(world); - } -} - -impl EntityCommand for F -where - F: FnOnce(EntityWorldMut) + Send + 'static, -{ - fn apply(self, id: Entity, world: &mut World) { - self(world.entity_mut(id)); - } -} - -impl EntityCommand for F -where - F: FnOnce(Entity, &mut World) + Send + 'static, -{ - fn apply(self, id: Entity, world: &mut World) { - self(id, world); - } -} - -/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities. -/// If any entities do not exist in the world, this command will panic. -/// -/// This is more efficient than inserting the bundles individually. -#[track_caller] -fn insert_batch(batch: I, mode: InsertMode) -> impl Command -where - I: IntoIterator + Send + Sync + 'static, - B: Bundle, -{ - #[cfg(feature = "track_location")] - let caller = Location::caller(); - move |world: &mut World| { - world.insert_batch_with_caller( - batch, - mode, - #[cfg(feature = "track_location")] - caller, - ); - } -} - -/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities. -/// If any entities do not exist in the world, this command will ignore them. -/// -/// This is more efficient than inserting the bundles individually. -#[track_caller] -fn try_insert_batch(batch: I, mode: InsertMode) -> impl Command -where - I: IntoIterator + Send + Sync + 'static, - B: Bundle, -{ - #[cfg(feature = "track_location")] - let caller = Location::caller(); - move |world: &mut World| { - world.try_insert_batch_with_caller( - batch, - mode, - #[cfg(feature = "track_location")] - caller, - ); - } -} - -/// A [`Command`] that despawns a specific entity. -/// This will emit a warning if the entity does not exist. -/// -/// # Note -/// -/// This won't clean up external references to the entity (such as parent-child relationships -/// if you're using `bevy_hierarchy`), which may leave the world in an invalid state. -#[track_caller] -fn despawn(log_warning: bool) -> impl EntityCommand { - let caller = Location::caller(); - move |entity: Entity, world: &mut World| { - world.despawn_with_caller(entity, caller, log_warning); - } -} - -/// An [`EntityCommand`] that adds the components in a [`Bundle`] to an entity. -#[track_caller] -fn insert(bundle: T, mode: InsertMode) -> impl EntityCommand { - let caller = Location::caller(); - move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.insert_with_caller( - bundle, - mode, - #[cfg(feature = "track_location")] - caller, - ); - } else { - panic!("error[B0003]: {caller}: Could not insert a bundle (of type `{}`) for entity {entity}, which {}. See: https://bevyengine.org/learn/errors/b0003", core::any::type_name::(), world.entities().entity_does_not_exist_error_details_message(entity)); - } - } -} - -/// An [`EntityCommand`] that attempts to add the components in a [`Bundle`] to an entity. -/// Does nothing if the entity does not exist. -#[track_caller] -fn try_insert(bundle: impl Bundle, mode: InsertMode) -> impl EntityCommand { - #[cfg(feature = "track_location")] - let caller = Location::caller(); - move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.insert_with_caller( - bundle, - mode, - #[cfg(feature = "track_location")] - caller, - ); - } - } -} - #[cfg(test)] #[allow(clippy::float_cmp, clippy::approx_constant)] mod tests { diff --git a/crates/bevy_ecs/src/world/command_queue.rs b/crates/bevy_ecs/src/world/command_queue.rs index 2032aaeb74c02..63c6bcf2d180e 100644 --- a/crates/bevy_ecs/src/world/command_queue.rs +++ b/crates/bevy_ecs/src/world/command_queue.rs @@ -2,6 +2,7 @@ use crate::system::{SystemBuffer, SystemMeta}; use core::{ fmt::Debug, + marker::PhantomData, mem::{size_of, MaybeUninit}, panic::AssertUnwindSafe, ptr::{addr_of_mut, NonNull}, @@ -11,7 +12,7 @@ use alloc::{boxed::Box, vec::Vec}; use bevy_ptr::{OwningPtr, Unaligned}; use log::warn; -use crate::world::{Command, World}; +use crate::{system::Command, world::World}; use super::DeferredWorld; @@ -75,9 +76,10 @@ unsafe impl Sync for CommandQueue {} impl CommandQueue { /// Push a [`Command`] onto the queue. #[inline] - pub fn push(&mut self, command: C) + pub fn push(&mut self, command: C) where - C: Command, + C: Command, + M: 'static, { // SAFETY: self is guaranteed to live for the lifetime of this method unsafe { @@ -154,17 +156,23 @@ impl RawCommandQueue { /// /// * Caller ensures that `self` has not outlived the underlying queue #[inline] - pub unsafe fn push(&mut self, command: C) + pub unsafe fn push(&mut self, command: C) where - C: Command, + C: Command, + M: 'static, { // Stores a command alongside its metadata. // `repr(C)` prevents the compiler from reordering the fields, // while `repr(packed)` prevents the compiler from inserting padding bytes. #[repr(C, packed)] - struct Packed { + struct Packed + where + C: Command, + M: 'static, + { meta: CommandMeta, - command: T, + command: C, + phantom: PhantomData, } let meta = CommandMeta { @@ -179,7 +187,7 @@ impl RawCommandQueue { Some(mut world) => { // SAFETY: Caller ensures pointer is not null let world = unsafe { world.as_mut() }; - command.apply(world); + _ = command.apply(world); // The command may have queued up world commands, which we flush here to ensure they are also picked up. // If the current command queue already the World Command queue, this will still behave appropriately because the global cursor // is still at the current `stop`, ensuring only the newly queued Commands will be applied. @@ -197,7 +205,7 @@ impl RawCommandQueue { let old_len = bytes.len(); // Reserve enough bytes for both the metadata and the command itself. - bytes.reserve(size_of::>()); + bytes.reserve(size_of::>()); // Pointer to the bytes at the end of the buffer. // SAFETY: We know it is within bounds of the allocation, due to the call to `.reserve()`. @@ -209,15 +217,18 @@ impl RawCommandQueue { // The call to `reserve()` ensures that the buffer has enough space to fit a value of type `C`, // and it is valid to write any bit pattern since the underlying buffer is of type `MaybeUninit`. unsafe { - ptr.cast::>() - .write_unaligned(Packed { meta, command }); + ptr.cast::>().write_unaligned(Packed { + meta, + command, + phantom: PhantomData, + }); } // Extend the length of the buffer to include the data we just wrote. // SAFETY: The new length is guaranteed to fit in the vector's capacity, // due to the call to `.reserve()` above. unsafe { - bytes.set_len(old_len + size_of::>()); + bytes.set_len(old_len + size_of::>()); } } @@ -344,8 +355,7 @@ impl SystemBuffer for CommandQueue { #[cfg(test)] mod test { use super::*; - use crate as bevy_ecs; - use crate::system::Resource; + use crate::{self as bevy_ecs, result::Result, system::Resource}; use alloc::{borrow::ToOwned, string::String, sync::Arc}; use core::{ panic::AssertUnwindSafe, @@ -371,7 +381,9 @@ mod test { } impl Command for DropCheck { - fn apply(self, _: &mut World) {} + fn apply(self, _: &mut World) -> Result { + Ok(()) + } } #[test] @@ -418,8 +430,9 @@ mod test { struct SpawnCommand; impl Command for SpawnCommand { - fn apply(self, world: &mut World) { + fn apply(self, world: &mut World) -> Result { world.spawn_empty(); + Ok(()) } } @@ -447,7 +460,7 @@ mod test { #[allow(dead_code)] struct PanicCommand(String); impl Command for PanicCommand { - fn apply(self, _: &mut World) { + fn apply(self, _: &mut World) -> Result { panic!("command is panicking"); } } @@ -523,7 +536,9 @@ mod test { #[allow(dead_code)] struct CommandWithPadding(u8, u16); impl Command for CommandWithPadding { - fn apply(self, _: &mut World) {} + fn apply(self, _: &mut World) -> Result { + Ok(()) + } } #[cfg(miri)] diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 16948c5a9f740..a13f6dddd90dc 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -43,6 +43,7 @@ use crate::{ observer::Observers, query::{DebugCheckedUnwrap, QueryData, QueryFilter, QueryState}, removal_detection::RemovedComponentEvents, + result::Result, schedule::{Schedule, ScheduleLabel, Schedules}, storage::{ResourceData, Storages}, system::{Commands, Resource}, @@ -69,42 +70,6 @@ use core::panic::Location; use unsafe_world_cell::{UnsafeEntityCell, UnsafeWorldCell}; -/// A [`World`] mutation. -/// -/// Should be used with [`Commands::queue`]. -/// -/// # Usage -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # use bevy_ecs::world::Command; -/// // Our world resource -/// #[derive(Resource, Default)] -/// struct Counter(u64); -/// -/// // Our custom command -/// struct AddToCounter(u64); -/// -/// impl Command for AddToCounter { -/// fn apply(self, world: &mut World) { -/// let mut counter = world.get_resource_or_insert_with(Counter::default); -/// counter.0 += self.0; -/// } -/// } -/// -/// fn some_system(mut commands: Commands) { -/// commands.queue(AddToCounter(42)); -/// } -/// ``` -pub trait Command: Send + 'static { - /// Applies this command, causing it to mutate the provided `world`. - /// - /// This method is used to define what a command "does" when it is ultimately applied. - /// Because this method takes `self`, you can store data or settings on the type that implements this trait. - /// This data is set by the system or other source of the command, and then ultimately read in this method. - fn apply(self, world: &mut World); -} - /// Stores and exposes operations on [entities](Entity), [components](Component), resources, /// and their associated metadata. /// diff --git a/crates/bevy_hierarchy/src/child_builder.rs b/crates/bevy_hierarchy/src/child_builder.rs index 8dd9cd9e5d017..5c8750540937d 100644 --- a/crates/bevy_hierarchy/src/child_builder.rs +++ b/crates/bevy_hierarchy/src/child_builder.rs @@ -3,8 +3,8 @@ use bevy_ecs::{ bundle::Bundle, entity::Entity, event::Events, - system::{Commands, EntityCommands}, - world::{Command, EntityWorldMut, World}, + system::{Command, Commands, EntityCommands}, + world::{EntityWorldMut, World}, }; use smallvec::{smallvec, SmallVec}; @@ -459,11 +459,19 @@ impl ChildBuild for WorldChildBuilder<'_> { } fn queue_command(&mut self, command: C) -> &mut Self { - command.apply(self.world); + self.world.commands().queue(command); self } } +impl WorldChildBuilder<'_> { + /// Calls the world's [`World::flush`] to apply any commands + /// queued by [`Self::queue_command`]. + pub fn flush_world(&mut self) { + self.world.flush(); + } +} + impl BuildChildren for EntityWorldMut<'_> { type Builder<'a> = WorldChildBuilder<'a>; diff --git a/crates/bevy_hierarchy/src/hierarchy.rs b/crates/bevy_hierarchy/src/hierarchy.rs index 7ab31a588535c..a71b7def8ad82 100644 --- a/crates/bevy_hierarchy/src/hierarchy.rs +++ b/crates/bevy_hierarchy/src/hierarchy.rs @@ -250,7 +250,7 @@ mod tests { use alloc::{borrow::ToOwned, string::String, vec, vec::Vec}; use bevy_ecs::{ component::Component, - system::Commands, + system::{error_handler, Commands}, world::{CommandQueue, World}, }; @@ -310,6 +310,7 @@ mod tests { { let mut commands = Commands::new(&mut queue, &world); + commands.override_error_handler(error_handler::silent()); commands.entity(parent_entity).despawn_recursive(); // despawning the same entity twice should not panic commands.entity(parent_entity).despawn_recursive();