diff --git a/Cargo.toml b/Cargo.toml index 65bf72603684f..d0a8ba6814428 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -303,6 +303,8 @@ bytemuck = "1.7" futures-lite = "2.0.1" crossbeam-channel = "0.5.0" argh = "0.1.12" +# Needed for command handler example +tracing-subscriber = "0.3.18" [[example]] name = "hello_world" @@ -1352,6 +1354,16 @@ description = "Groups commonly used compound queries and query filters into a si category = "ECS (Entity Component System)" wasm = false +[[example]] +name = "command_error_handling" +path = "examples/ecs/command_error_handling.rs" + +[package.metadata.example.command_error_handling] +name = "Command Error Handling" +description = "Command error handling" +category = "ECS (Entity Component System)" +wasm = false + [[example]] name = "dynamic" path = "examples/ecs/dynamic.rs" diff --git a/benches/benches/bevy_ecs/world/commands.rs b/benches/benches/bevy_ecs/world/commands.rs index 70cf1351acfcb..d350b60ee2a66 100644 --- a/benches/benches/bevy_ecs/world/commands.rs +++ b/benches/benches/bevy_ecs/world/commands.rs @@ -1,7 +1,7 @@ use bevy_ecs::{ component::Component, entity::Entity, - system::{Command, CommandQueue, Commands}, + system::{Command, CommandQueue, Commands, CommandErrorHandler}, world::World, }; use criterion::{black_box, Criterion}; @@ -69,6 +69,49 @@ pub fn spawn_commands(criterion: &mut Criterion) { group.finish(); } +pub fn spawn_commands_error_handler(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("spawn_commands_error_handler"); + group.warm_up_time(std::time::Duration::from_millis(500)); + group.measurement_time(std::time::Duration::from_secs(4)); + + for entity_count in (1..5).map(|i| i * 2 * 1000) { + group.bench_function(format!("{}_entities", entity_count), |bencher| { + let mut world = World::default(); + let mut command_queue = CommandQueue::default(); + + bencher.iter(|| { + let mut commands = Commands::new(&mut command_queue, &world); + for i in 0..entity_count { + let mut entity = commands.spawn_empty(); + + if black_box(i % 2 == 0) { + entity.insert(A).on_err(CommandErrorHandler::log); + } + + if black_box(i % 3 == 0) { + entity.insert(B).on_err(CommandErrorHandler::ignore); + } + + if black_box(i % 4 == 0) { + entity.insert(C).on_err(|err, _ctx| { + println!("Error: {:?}", err); + }); + } + + if black_box(i % 5 == 0) { + entity.despawn().on_err(CommandErrorHandler::log); + } + } + command_queue.apply(&mut world); + }); + }); + } + + group.finish(); +} + + + #[derive(Default, Component)] struct Matrix([[f32; 4]; 4]); diff --git a/benches/benches/bevy_ecs/world/mod.rs b/benches/benches/bevy_ecs/world/mod.rs index f594d082d7fb0..4318a3765d21c 100644 --- a/benches/benches/bevy_ecs/world/mod.rs +++ b/benches/benches/bevy_ecs/world/mod.rs @@ -12,6 +12,7 @@ criterion_group!( world_benches, empty_commands, spawn_commands, + spawn_commands_error_handler, insert_commands, fake_commands, zero_sized_commands, diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 0260860c75e27..273f6fc9ced7e 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -46,8 +46,9 @@ pub mod prelude { OnTransition, Schedule, Schedules, State, StateTransitionEvent, States, SystemSet, }, system::{ - Commands, Deferred, In, IntoSystem, Local, NonSend, NonSendMut, ParallelCommands, - ParamSet, Query, ReadOnlySystem, Res, ResMut, Resource, System, SystemParamFunction, + CommandErrorHandler, Commands, Deferred, FallibleCommand, In, IntoSystem, Local, + NonSend, NonSendMut, ParallelCommands, ParamSet, Query, ReadOnlySystem, Res, ResMut, + Resource, System, SystemParamFunction, }, world::{EntityMut, EntityRef, EntityWorldMut, FromWorld, World}, }; diff --git a/crates/bevy_ecs/src/system/commands/config.rs b/crates/bevy_ecs/src/system/commands/config.rs new file mode 100644 index 0000000000000..5a5fc3df0b15a --- /dev/null +++ b/crates/bevy_ecs/src/system/commands/config.rs @@ -0,0 +1,204 @@ +use crate::{ + prelude::{FallibleCommand, World}, + system::Command, +}; +use bevy_utils::tracing::error; +use std::{ + fmt::Debug, + ops::{Deref, DerefMut}, +}; + +#[doc(hidden)] +pub trait AddCommand { + fn add_command(&mut self, command: impl Command); +} + +/// Provides configuration mechanisms in case a command errors. +/// You can specify a custom handler via [`CommandErrorHandler`] or +/// use one of the provided implementations. +/// +/// ## Note +/// The default error handler logs the error (via [`error!`]), but does not panic. +pub struct FallibleCommandConfig<'a, C, T> +where + C: FallibleCommand, + T: AddCommand, +{ + command: Option, + inner: &'a mut T, +} + +impl<'a, C, T> Deref for FallibleCommandConfig<'a, C, T> +where + C: FallibleCommand, + T: AddCommand, +{ + type Target = T; + + #[inline] + fn deref(&self) -> &Self::Target { + self.inner + } +} + +impl<'a, C, T> DerefMut for FallibleCommandConfig<'a, C, T> +where + C: FallibleCommand, + T: AddCommand, +{ + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + self.inner + } +} + +/// Builtin command error handlers. +pub struct CommandErrorHandler; + +impl CommandErrorHandler { + /// If the command failed, log the error. + /// + /// ## Note + /// This is the default behavior if no error handler is specified. + pub fn log(error: E, _ctx: CommandContext) { + error!("Commands failed with error: {:?}", error); + } + + /// If the command failed, [`panic!`] with the error. + pub fn panic(error: E, _ctx: CommandContext) { + panic!("Commands failed with error: {:?}", error) + } + + /// If the command failed, ignore the error and silently succeed. + pub fn ignore(_error: E, _ctx: CommandContext) {} +} + +pub(crate) struct HandledErrorCommand +where + C: FallibleCommand, + F: FnOnce(C::Error, CommandContext) + Send + Sync + 'static, +{ + pub(crate) command: C, + pub(crate) error_handler: F, +} + +impl Command for HandledErrorCommand +where + C: FallibleCommand, + F: FnOnce(C::Error, CommandContext) + Send + Sync + 'static, +{ + fn apply(self, world: &mut World) { + let HandledErrorCommand { + command, + error_handler, + } = self; + + if let Err(error) = command.try_apply(world) { + error_handler(error, CommandContext { world }); + } + } +} + +#[non_exhaustive] +/// Context passed to [`CommandErrorHandler`]. +pub struct CommandContext<'a> { + /// The [`World`] the command was applied to. + pub world: &'a mut World, +} + +/// Similar to [`FallibleCommandConfig`] however does not +/// implement [`DerefMut`] nor return `&mut T` of the underlying +/// Commands type. +pub struct FinalFallibleCommandConfig<'a, C, T> +where + C: FallibleCommand, + T: AddCommand, +{ + command: Option, + inner: &'a mut T, +} + +macro_rules! impl_fallible_commands { + ($name:ident, $returnty:ty, $returnfunc:ident) => { + impl<'a, C, T> $name<'a, C, T> + where + C: FallibleCommand, + C::Error: Debug, + T: AddCommand, + { + #[inline] + pub(crate) fn new(command: C, inner: &'a mut T) -> Self { + Self { + command: Some(command), + inner, + } + } + + #[inline] + #[allow(dead_code)] + fn return_inner(&mut self) -> &mut T { + self.inner + } + + #[inline] + #[allow(dead_code)] + fn return_unit(&self) {} + + /// If the command failed, run the provided `error_handler`. + /// + /// ## Note + /// This is normally used in conjunction with [`CommandErrorHandler`]. + /// However, this can also be used with custom error handlers (e.g. closures). + /// + /// # Examples + /// ``` + /// use bevy_ecs::prelude::*; + /// + /// #[derive(Component, Resource)] + /// struct TestComponent(pub u32); + /// + /// fn system(mut commands: Commands) { + /// // built-in error handler + /// commands.spawn_empty().insert(TestComponent(42)).on_err(CommandErrorHandler::ignore); + /// + /// // custom error handler + /// commands.spawn_empty().insert(TestComponent(42)).on_err(|error, ctx| {}); + /// } + /// ``` + #[inline] + pub fn on_err( + &mut self, + error_handler: impl FnOnce(C::Error, CommandContext) + Send + Sync + 'static, + ) -> $returnty { + let command = self + .command + .take() + .expect("Cannot call `on_err` multiple times for a command error handler."); + self.inner.add_command(HandledErrorCommand { + command, + error_handler, + }); + self.$returnfunc() + } + } + + impl<'a, C, T> Drop for $name<'a, C, T> + where + C: FallibleCommand, + T: AddCommand, + { + #[inline] + fn drop(&mut self) { + if let Some(command) = self.command.take() { + self.inner.add_command(HandledErrorCommand { + command, + error_handler: CommandErrorHandler::log, + }); + } + } + } + }; +} + +impl_fallible_commands!(FinalFallibleCommandConfig, (), return_unit); +impl_fallible_commands!(FallibleCommandConfig, &mut T, return_inner); diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index d2ebdce1a06f6..9dd5816ce162c 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1,4 +1,5 @@ mod command_queue; +mod config; mod parallel_scope; use crate::{ @@ -11,8 +12,9 @@ use crate::{ use bevy_ecs_macros::SystemParam; use bevy_utils::tracing::{error, info}; pub use command_queue::CommandQueue; +pub use config::*; pub use parallel_scope::*; -use std::marker::PhantomData; +use std::{fmt::Debug, marker::PhantomData}; use super::{Deferred, Resource, SystemBuffer, SystemMeta}; @@ -52,6 +54,21 @@ pub trait Command: Send + 'static { fn apply(self, world: &mut World); } +/// A [`World`] mutation that can potentially fail. +/// For an infallible variant, use [`Command`]. +pub trait FallibleCommand: Send + Sync + 'static { + /// The type of error that this command may return. + type Error: Debug; + + /// Like `[Command::apply]` it applies this command causing it to mutate the provided `world` but + /// it won't panic if an error occurs. + /// + /// 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 try_apply(self, world: &mut World) -> Result<(), Self::Error>; +} + /// A [`Command`] queue to perform structural changes to the [`World`]. /// /// Since each command requires exclusive access to the `World`, @@ -543,8 +560,16 @@ impl<'w, 's> Commands<'w, 's> { /// # } /// # bevy_ecs::system::assert_is_system(system); /// ``` - pub fn remove_resource(&mut self) { - self.queue.push(remove_resource::); + pub fn remove_resource( + &mut self, + ) -> FinalFallibleCommandConfig<'_, RemoveResource, Self> { + // self.queue.push(RemoveResource::::new()); + FinalFallibleCommandConfig::new( + RemoveResource { + _marker: PhantomData, + }, + self, + ) } /// Runs the system corresponding to the given [`SystemId`]. @@ -610,6 +635,20 @@ impl<'w, 's> Commands<'w, 's> { pub fn add(&mut self, command: C) { self.queue.push(command); } + + /// Adds a fallible command to the command list. + pub fn add_fallible(&mut self, command: C) -> FinalFallibleCommandConfig<'_, C, Self> + where + C: FallibleCommand, + { + FinalFallibleCommandConfig::new(command, self) + } +} + +impl<'a, 'w> AddCommand for Commands<'a, 'w> { + fn add_command(&mut self, command: impl Command) { + self.add(command); + } } /// A [`Command`] which gets executed for a given [`Entity`]. @@ -783,8 +822,14 @@ impl EntityCommands<'_> { /// } /// # bevy_ecs::system::assert_is_system(add_combat_stats_system); /// ``` - pub fn insert(&mut self, bundle: impl Bundle) -> &mut Self { - self.add(insert(bundle)) + pub fn insert(&mut self, bundle: T) -> FallibleCommandConfig<'_, Insert, Self> { + FallibleCommandConfig::new( + Insert { + entity: self.entity, + bundle, + }, + self, + ) } /// Tries to add a [`Bundle`] of components to the entity. @@ -874,11 +919,17 @@ impl EntityCommands<'_> { /// } /// # bevy_ecs::system::assert_is_system(remove_combat_stats_system); /// ``` - pub fn remove(&mut self) -> &mut Self + pub fn remove(&mut self) -> FallibleCommandConfig<'_, Remove, Self> where T: Bundle, { - self.add(remove::) + FallibleCommandConfig::new( + Remove { + entity: self.entity, + _marker: PhantomData, + }, + self, + ) } /// Despawns the entity. @@ -911,8 +962,13 @@ impl EntityCommands<'_> { /// } /// # bevy_ecs::system::assert_is_system(remove_character_system); /// ``` - pub fn despawn(&mut self) { - self.add(despawn); + pub fn despawn(&mut self) -> FinalFallibleCommandConfig<'_, Despawn, Self> { + FinalFallibleCommandConfig::new( + Despawn { + entity: self.entity, + }, + self, + ) } /// Pushes an [`EntityCommand`] to the queue, which will get executed for the current [`Entity`]. @@ -973,11 +1029,17 @@ impl EntityCommands<'_> { /// } /// # bevy_ecs::system::assert_is_system(remove_combat_stats_system); /// ``` - pub fn retain(&mut self) -> &mut Self + pub fn retain(&mut self) -> FallibleCommandConfig<'_, Retain, Self> where T: Bundle, { - self.add(retain::) + FallibleCommandConfig::new( + Retain { + entity: self.entity, + _marker: PhantomData, + }, + self, + ) } /// Logs the components of the entity at the info level. @@ -995,6 +1057,20 @@ impl EntityCommands<'_> { } } +fn try_insert(bundle: impl Bundle) -> impl EntityCommand { + move |entity, world: &mut World| { + if let Some(mut entity) = world.get_entity_mut(entity) { + entity.insert(bundle); + } + } +} + +impl<'a> AddCommand for EntityCommands<'a> { + fn add_command(&mut self, command: impl Command) { + self.commands.add_command(command); + } +} + impl Command for F where F: FnOnce(&mut World) + Send + 'static, @@ -1062,26 +1138,74 @@ where /// /// 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. -fn despawn(entity: Entity, world: &mut World) { - world.despawn(entity); +#[derive(Debug)] +pub struct Despawn { + /// The entity that will be despawned. + pub entity: Entity, } -/// An [`EntityCommand`] that adds the components in a [`Bundle`] to an entity. -fn insert(bundle: T) -> impl EntityCommand { - move |entity: Entity, world: &mut World| { - if let Some(mut entity) = world.get_entity_mut(entity) { - entity.insert(bundle); +/// The error resulting from [`EntityCommands::despawn`] +#[derive(Debug)] +pub struct DespawnError { + /// The entity that was supposed to get despawned. + pub entity: Entity, +} + +impl FallibleCommand for Despawn { + type Error = DespawnError; + + fn try_apply(self, world: &mut World) -> Result<(), Self::Error> { + if world.despawn(self.entity) { + Ok(()) } else { - panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {:?} because it doesn't exist in this World.", std::any::type_name::(), entity); + Err(DespawnError { + entity: self.entity, + }) } } } -/// An [`EntityCommand`] that attempts to add the components in a [`Bundle`] to an entity. -fn try_insert(bundle: impl Bundle) -> impl EntityCommand { - move |entity, world: &mut World| { - if let Some(mut entity) = world.get_entity_mut(entity) { - entity.insert(bundle); +/// A [`Command`] that adds the components in a [`Bundle`] to an entity. +pub struct Insert { + /// The entity to which the components will be added. + pub entity: Entity, + /// The [`Bundle`] containing the components that will be added to the entity. + pub bundle: T, +} + +/// The error resulting from [`EntityCommands::insert`] +/// Contains both the failed to insert component and the relative entity. +pub struct InsertError { + /// The entity that was supposed to get the bundle inserted. + pub entity: Entity, + /// The bundle that was supposed to get inserted. + pub bundle: T, +} + +impl Debug for InsertError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("InsertError") + .field("entity", &self.entity) + .field("bundle_type", &std::any::type_name::()) + .finish() + } +} + +impl FallibleCommand for Insert +where + T: Bundle + 'static, +{ + type Error = InsertError; + + fn try_apply(self, world: &mut World) -> Result<(), Self::Error> { + if let Some(mut entity_mut) = world.get_entity_mut(self.entity) { + entity_mut.insert(self.bundle); + Ok(()) + } else { + Err(InsertError { + entity: self.entity, + bundle: self.bundle, + }) } } } @@ -1089,18 +1213,111 @@ fn try_insert(bundle: impl Bundle) -> impl EntityCommand { /// A [`Command`] that removes components from an entity. /// For a [`Bundle`] type `T`, this will remove any components in the bundle. /// Any components in the bundle that aren't found on the entity will be ignored. -fn remove(entity: Entity, world: &mut World) { - if let Some(mut entity_mut) = world.get_entity_mut(entity) { - entity_mut.remove::(); +#[derive(Debug)] +pub struct Remove { + /// The entity from which the components will be removed. + pub entity: Entity, + _marker: PhantomData, +} + +/// The error resulting from [`EntityCommands::remove`] +pub struct RemoveError { + /// The entity that was supposed to get the bundle removed. + pub entity: Entity, + phantom: PhantomData, +} + +impl Debug for RemoveError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RemoveError") + .field("entity", &self.entity) + .field("component_type", &std::any::type_name::()) + .finish() + } +} + +impl FallibleCommand for Remove +where + T: Bundle, +{ + type Error = RemoveError; + + fn try_apply(self, world: &mut World) -> Result<(), Self::Error> { + if let Some(mut entity_mut) = world.get_entity_mut(self.entity) { + entity_mut.remove::(); + Ok(()) + } else { + Err(RemoveError { + entity: self.entity, + phantom: PhantomData, + }) + } + } +} + +impl Remove { + /// Creates a [`Command`] which will remove the specified [`Entity`] when applied. + pub const fn new(entity: Entity) -> Self { + Self { + entity, + _marker: PhantomData, + } } } /// A [`Command`] that removes components from an entity. /// For a [`Bundle`] type `T`, this will remove all components except those in the bundle. /// Any components in the bundle that aren't found on the entity will be ignored. -fn retain(entity: Entity, world: &mut World) { - if let Some(mut entity_mut) = world.get_entity_mut(entity) { - entity_mut.retain::(); +#[derive(Debug)] +pub struct Retain { + /// The entity from which the components will be removed. + pub entity: Entity, + _marker: PhantomData, +} + +/// The error resulting from [`EntityCommands::retain`] +pub struct RemoveRetainError { + /// The entity that was supposed to get the bundle retained. + pub entity: Entity, + _marker: PhantomData, +} + +impl Debug for RemoveRetainError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RemoveRetainError") + .field("entity", &self.entity) + .field("component_type", &std::any::type_name::()) + .finish() + } +} + +impl FallibleCommand for Retain +where + T: Bundle, +{ + type Error = RemoveRetainError; + + fn try_apply(self, world: &mut World) -> Result<(), Self::Error> { + match world.get_entity_mut(self.entity) { + Some(mut entity_mut) => { + entity_mut.retain::(); + Ok(()) + } + None => Err(RemoveRetainError { + entity: self.entity, + _marker: PhantomData, + }), + } + } +} + +impl Retain { + /// Creates a [`Command`] which will remove all but the specified components when applied. + pub const fn new(entity: Entity) -> Self { + Self { + entity, + _marker: PhantomData, + } } } @@ -1111,8 +1328,35 @@ fn init_resource(world: &mut World) { } /// A [`Command`] that removes the [resource](Resource) `R` from the world. -fn remove_resource(world: &mut World) { - world.remove_resource::(); +pub struct RemoveResource { + _marker: PhantomData, +} + +/// The error resulting from [`Commands::remove_resource`] +pub struct RemoveResourceError { + phantom: PhantomData, +} + +impl Debug for RemoveResourceError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RemoveResourceError") + .field("resource_type", &std::any::type_name::()) + .finish() + } +} + +impl FallibleCommand for RemoveResource { + type Error = RemoveResourceError; + + fn try_apply(self, world: &mut World) -> Result<(), Self::Error> { + if world.remove_resource::().is_some() { + Ok(()) + } else { + Err(RemoveResourceError { + phantom: PhantomData, + }) + } + } } /// A [`Command`] that inserts a [`Resource`] into the world. @@ -1138,7 +1382,8 @@ mod tests { use crate::{ self as bevy_ecs, component::Component, - system::{CommandQueue, Commands, Resource}, + entity::Entity, + system::{CommandErrorHandler, CommandQueue, Commands, FallibleCommand, Resource}, world::World, }; use std::sync::{ @@ -1311,4 +1556,138 @@ mod tests { assert!(world.contains_resource::>()); assert!(world.contains_resource::>()); } + + struct FailingCommand; + impl FallibleCommand for FailingCommand { + type Error = (); + + fn try_apply(self, _: &mut World) -> Result<(), Self::Error> { + Err(()) + } + } + + struct SuccessfulCommand; + impl FallibleCommand for SuccessfulCommand { + type Error = (); + + fn try_apply(self, _: &mut World) -> Result<(), Self::Error> { + Ok(()) + } + } + + #[test] + fn test_commands_error_handler() { + let invoked = Arc::new(AtomicUsize::new(0)); + let mut world = World::default(); + let mut queue = CommandQueue::default(); + { + let mut commands = Commands::new(&mut queue, &world); + + commands.insert_resource(W(42u32)); + let invoked_clone = invoked.clone(); + // should succeed + commands.remove_resource::>().on_err(move |_, _| { + invoked_clone.fetch_add(1, Ordering::Relaxed); + }); + + let invoked_clone = invoked.clone(); + // should fail + commands.remove_resource::>().on_err(move |_, _| { + invoked_clone.fetch_add(1, Ordering::Relaxed); + }); + + let invoked_clone = invoked.clone(); + // should fail + commands.add_fallible(FailingCommand).on_err(move |_, _| { + invoked_clone.fetch_add(1, Ordering::Relaxed); + }); + + let invoked_clone = invoked.clone(); + // should succeed + commands + .add_fallible(SuccessfulCommand) + .on_err(move |_, _| { + invoked_clone.fetch_add(1, Ordering::Relaxed); + }); + } + queue.apply(&mut world); + + assert_eq!(invoked.load(Ordering::Relaxed), 2); + } + + #[test] + fn test_entity_commands_error_handler() { + #[derive(Component)] + struct TestComponent { + value: u32, + } + + let invoked = Arc::new(AtomicUsize::new(0)); + + let mut world = World::default(); + + let valid_entity = world.spawn_empty().id(); + let entity_for_checking_despawning = world.spawn_empty().id(); + + let mut queue = CommandQueue::default(); + { + let mut commands = Commands::new(&mut queue, &world); + + // EntityCommands::despawn + let mut try_despawn = |e| { + let invoked_clone = invoked.clone(); + commands.entity(e).despawn().on_err(move |error, _| { + assert_eq!(error.entity, e); + invoked_clone.fetch_add(1, Ordering::Relaxed); + }); + }; + + try_despawn(entity_for_checking_despawning); + try_despawn(entity_for_checking_despawning); + try_despawn(valid_entity); + + // EntityCommands::insert + let invoked_clone = invoked.clone(); + commands + .entity(valid_entity) + .insert(TestComponent { value: 42 }) + .on_err(move |error, _| { + assert_eq!(error.entity, valid_entity); + assert_eq!(error.bundle.value, 42); + invoked_clone.fetch_add(1, Ordering::Relaxed); + }); + + // EntityCommands::remove_resource + let invoked_clone = invoked.clone(); + commands + .entity(valid_entity) + .remove::() + .on_err(move |error, _| { + assert_eq!(error.entity, valid_entity); + invoked_clone.fetch_add(1, Ordering::Relaxed); + }); + } + queue.apply(&mut world); + + assert_eq!(invoked.load(Ordering::Relaxed), 3); + } + + #[test] + #[should_panic] + fn test_panicking_error_handler() { + std::panic::set_hook(Box::new(|_| {})); // prevents printing of stack trace. + + let mut world = World::default(); + let mut queue = CommandQueue::default(); + { + let mut commands = Commands::new(&mut queue, &world); + let invalid_entity = + Entity::from_raw_and_generation(42, std::num::NonZeroU32::new(0).unwrap()); + commands + .entity(invalid_entity) + .despawn() + .on_err(CommandErrorHandler::panic); + } + queue.apply(&mut world); + } } diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 1bb0e74d0b04f..8e0567ccd6dc9 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -885,7 +885,6 @@ impl World { entity.despawn(); true } else { - warn!("error[B0003]: Could not despawn entity {:?} because it doesn't exist in this World.", entity); false } } diff --git a/examples/README.md b/examples/README.md index e03d0a0db27d2..217a2263f5849 100644 --- a/examples/README.md +++ b/examples/README.md @@ -223,6 +223,7 @@ Example | Description Example | Description --- | --- +[Command Error Handling](../examples/ecs/command_error_handling.rs) | Command error handling [Component Change Detection](../examples/ecs/component_change_detection.rs) | Change detection on components [Custom Query Parameters](../examples/ecs/custom_query_param.rs) | Groups commonly used compound queries and query filters into a single type [Dynamic ECS](../examples/ecs/dynamic.rs) | Dynamically create components, spawn entities with those components and query those components diff --git a/examples/ecs/command_error_handling.rs b/examples/ecs/command_error_handling.rs new file mode 100644 index 0000000000000..c73dd191046c3 --- /dev/null +++ b/examples/ecs/command_error_handling.rs @@ -0,0 +1,86 @@ +use bevy::prelude::*; + +fn main() { + tracing_subscriber::fmt::init(); + + App::new() + .insert_resource(FailedDespawnAttempts(0)) + .add_systems(Startup, setup) + .add_systems( + Update, + ( + despawn_all_entities, + remove_components, + log_failed_despawn_attempts.after(despawn_all_entities), + ), + ) + .run(); +} + +#[derive(Component)] +struct A(usize); + +#[derive(Component, Default)] +struct B(u32); + +#[derive(Resource)] +struct FailedDespawnAttempts(usize); + +fn setup(mut commands: Commands) { + for i in 0..3 { + // Note that `insert` is a fallible function. + // If no error handler is specified, the default behavior is to log the error, and continue. + // However, these calls to `insert` and `insert_bundle` will not fail, since the entity is valid. + commands.spawn_empty().insert(A(i)).insert(B::default()); + } +} + +fn log_failed_despawn_attempts(attempts: Res) { + info!("There have been {} failed despawn attempts!", attempts.0); +} + +fn despawn_all_entities(mut commands: Commands, query: Query) { + info!("Despawning entities..."); + for e in query.iter() { + // `on_err` also allows you to provide a custom error handler! + commands + .entity(e) + .despawn() + .on_err(CommandErrorHandler::ignore); + } + + info!("Trying to despawn again..."); + for e in query.iter() { + // `on_err` also allows you to provide a custom error handler! + commands.entity(e).despawn().on_err(|error, ctx| { + // You'll notice that the `error` will also give you back the entity + // you tried to despawn. + let entity = error.entity; + + warn!("Sadly our entity '{:?}' didn't despawn :(", entity); + + // error handlers have mutable access to `World` + if let Some(mut failed_despawns) = ctx.world.get_resource_mut::() + { + failed_despawns.0 += 1; + } + }); + } +} + +fn remove_components(mut commands: Commands, query: Query) { + for e in query.iter() { + // Some nice things: + // - You can still chain commands! + // - There are a slew of built-in error handlers + commands + .entity(e) + .remove::() + // `CommandErrorHandler::ignore` will neither log nor panic the error + .on_err(CommandErrorHandler::ignore) + .remove::() + // `CommandErrorHandler::log` is the default behavior, and will log the error. + // `CommandErrorHandler::panic` is another alternative which will panic on the error. + .on_err(CommandErrorHandler::log); + } +}