feat(ecs): implement fallible observer systems (#17731)
This commit builds on top of the work done in #16589 and #17051, by adding support for fallible observer systems. As with the previous work, the actual results of the observer system are suppressed for now, but the intention is to provide a way to handle errors in a global way. Until then, you can use a `PipeSystem` to manually handle results. --------- Signed-off-by: Jean Mertz <git@jeanmertz.com>
This commit is contained in:
parent
5b0d898866
commit
7d8504f30e
@ -2165,6 +2165,7 @@ name = "Fallible Systems"
|
|||||||
description = "Systems that return results to handle errors"
|
description = "Systems that return results to handle errors"
|
||||||
category = "ECS (Entity Component System)"
|
category = "ECS (Entity Component System)"
|
||||||
wasm = false
|
wasm = false
|
||||||
|
required-features = ["bevy_mesh_picking_backend"]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "startup_system"
|
name = "startup_system"
|
||||||
|
@ -6,6 +6,7 @@ use crate::{
|
|||||||
observer::{ObserverDescriptor, ObserverTrigger},
|
observer::{ObserverDescriptor, ObserverTrigger},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
query::DebugCheckedUnwrap,
|
query::DebugCheckedUnwrap,
|
||||||
|
result::{DefaultSystemErrorHandler, SystemErrorContext},
|
||||||
system::{IntoObserverSystem, ObserverSystem},
|
system::{IntoObserverSystem, ObserverSystem},
|
||||||
world::DeferredWorld,
|
world::DeferredWorld,
|
||||||
};
|
};
|
||||||
@ -272,6 +273,7 @@ pub struct Observer {
|
|||||||
system: Box<dyn Any + Send + Sync + 'static>,
|
system: Box<dyn Any + Send + Sync + 'static>,
|
||||||
descriptor: ObserverDescriptor,
|
descriptor: ObserverDescriptor,
|
||||||
hook_on_add: ComponentHook,
|
hook_on_add: ComponentHook,
|
||||||
|
error_handler: Option<fn(Error, SystemErrorContext)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Observer {
|
impl Observer {
|
||||||
@ -282,6 +284,7 @@ impl Observer {
|
|||||||
system: Box::new(IntoObserverSystem::into_system(system)),
|
system: Box::new(IntoObserverSystem::into_system(system)),
|
||||||
descriptor: Default::default(),
|
descriptor: Default::default(),
|
||||||
hook_on_add: hook_on_add::<E, B, I::System>,
|
hook_on_add: hook_on_add::<E, B, I::System>,
|
||||||
|
error_handler: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,6 +319,14 @@ impl Observer {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the error handler to use for this observer.
|
||||||
|
///
|
||||||
|
/// See the [`result` module-level documentation](crate::result) for more information.
|
||||||
|
pub fn with_error_handler(mut self, error_handler: fn(Error, SystemErrorContext)) -> Self {
|
||||||
|
self.error_handler = Some(error_handler);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the [`ObserverDescriptor`] for this [`Observer`].
|
/// Returns the [`ObserverDescriptor`] for this [`Observer`].
|
||||||
pub fn descriptor(&self) -> &ObserverDescriptor {
|
pub fn descriptor(&self) -> &ObserverDescriptor {
|
||||||
&self.descriptor
|
&self.descriptor
|
||||||
@ -363,6 +374,15 @@ fn observer_system_runner<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
|
|||||||
}
|
}
|
||||||
state.last_trigger_id = last_trigger;
|
state.last_trigger_id = last_trigger;
|
||||||
|
|
||||||
|
// SAFETY: Observer was triggered so must have an `Observer` component.
|
||||||
|
let error_handler = unsafe {
|
||||||
|
observer_cell
|
||||||
|
.get::<Observer>()
|
||||||
|
.debug_checked_unwrap()
|
||||||
|
.error_handler
|
||||||
|
.debug_checked_unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
let trigger: Trigger<E, B> = Trigger::new(
|
let trigger: Trigger<E, B> = Trigger::new(
|
||||||
// SAFETY: Caller ensures `ptr` is castable to `&mut T`
|
// SAFETY: Caller ensures `ptr` is castable to `&mut T`
|
||||||
unsafe { ptr.deref_mut() },
|
unsafe { ptr.deref_mut() },
|
||||||
@ -386,7 +406,15 @@ fn observer_system_runner<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
|
|||||||
unsafe {
|
unsafe {
|
||||||
(*system).update_archetype_component_access(world);
|
(*system).update_archetype_component_access(world);
|
||||||
if (*system).validate_param_unsafe(world) {
|
if (*system).validate_param_unsafe(world) {
|
||||||
(*system).run_unsafe(trigger, world);
|
if let Err(err) = (*system).run_unsafe(trigger, world) {
|
||||||
|
error_handler(
|
||||||
|
err,
|
||||||
|
SystemErrorContext {
|
||||||
|
name: (*system).name(),
|
||||||
|
last_run: (*system).get_last_run(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
(*system).queue_deferred(world.into_deferred());
|
(*system).queue_deferred(world.into_deferred());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -416,10 +444,15 @@ fn hook_on_add<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let error_handler = world.get_resource_or_init::<DefaultSystemErrorHandler>().0;
|
||||||
|
|
||||||
// Initialize System
|
// Initialize System
|
||||||
let system: *mut dyn ObserverSystem<E, B> =
|
let system: *mut dyn ObserverSystem<E, B> =
|
||||||
if let Some(mut observe) = world.get_mut::<Observer>(entity) {
|
if let Some(mut observe) = world.get_mut::<Observer>(entity) {
|
||||||
descriptor.merge(&observe.descriptor);
|
descriptor.merge(&observe.descriptor);
|
||||||
|
if observe.error_handler.is_none() {
|
||||||
|
observe.error_handler = Some(error_handler);
|
||||||
|
}
|
||||||
let system = observe.system.downcast_mut::<S>().unwrap();
|
let system = observe.system.downcast_mut::<S>().unwrap();
|
||||||
&mut *system
|
&mut *system
|
||||||
} else {
|
} else {
|
||||||
@ -442,3 +475,44 @@ fn hook_on_add<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::{event::Event, observer::Trigger};
|
||||||
|
|
||||||
|
#[derive(Event)]
|
||||||
|
struct TriggerEvent;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "I failed!")]
|
||||||
|
fn test_fallible_observer() {
|
||||||
|
fn system(_: Trigger<TriggerEvent>) -> Result {
|
||||||
|
Err("I failed!".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut world = World::default();
|
||||||
|
world.add_observer(system);
|
||||||
|
Schedule::default().run(&mut world);
|
||||||
|
world.trigger(TriggerEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fallible_observer_ignored_errors() {
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
struct Ran(bool);
|
||||||
|
|
||||||
|
fn system(_: Trigger<TriggerEvent>, mut ran: ResMut<Ran>) -> Result {
|
||||||
|
ran.0 = true;
|
||||||
|
Err("I failed!".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut world = World::default();
|
||||||
|
world.init_resource::<Ran>();
|
||||||
|
let observer = Observer::new(system).with_error_handler(crate::result::ignore);
|
||||||
|
world.spawn(observer);
|
||||||
|
Schedule::default().run(&mut world);
|
||||||
|
world.trigger(TriggerEvent);
|
||||||
|
assert!(world.resource::<Ran>().0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,22 +1,27 @@
|
|||||||
|
use alloc::{borrow::Cow, vec::Vec};
|
||||||
|
use core::marker::PhantomData;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
archetype::ArchetypeComponentId,
|
||||||
|
component::{ComponentId, Tick},
|
||||||
prelude::{Bundle, Trigger},
|
prelude::{Bundle, Trigger},
|
||||||
system::System,
|
query::Access,
|
||||||
|
result::Result,
|
||||||
|
schedule::{Fallible, Infallible},
|
||||||
|
system::{input::SystemIn, System},
|
||||||
|
world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::IntoSystem;
|
use super::IntoSystem;
|
||||||
|
|
||||||
/// Implemented for [`System`]s that have a [`Trigger`] as the first argument.
|
/// Implemented for [`System`]s that have a [`Trigger`] as the first argument.
|
||||||
pub trait ObserverSystem<E: 'static, B: Bundle, Out = ()>:
|
pub trait ObserverSystem<E: 'static, B: Bundle, Out = Result>:
|
||||||
System<In = Trigger<'static, E, B>, Out = Out> + Send + 'static
|
System<In = Trigger<'static, E, B>, Out = Out> + Send + 'static
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<
|
impl<E: 'static, B: Bundle, Out, T> ObserverSystem<E, B, Out> for T where
|
||||||
E: 'static,
|
T: System<In = Trigger<'static, E, B>, Out = Out> + Send + 'static
|
||||||
B: Bundle,
|
|
||||||
Out,
|
|
||||||
T: System<In = Trigger<'static, E, B>, Out = Out> + Send + 'static,
|
|
||||||
> ObserverSystem<E, B, Out> for T
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,7 +37,7 @@ impl<
|
|||||||
label = "the trait `IntoObserverSystem` is not implemented",
|
label = "the trait `IntoObserverSystem` is not implemented",
|
||||||
note = "for function `ObserverSystem`s, ensure the first argument is a `Trigger<T>` and any subsequent ones are `SystemParam`"
|
note = "for function `ObserverSystem`s, ensure the first argument is a `Trigger<T>` and any subsequent ones are `SystemParam`"
|
||||||
)]
|
)]
|
||||||
pub trait IntoObserverSystem<E: 'static, B: Bundle, M, Out = ()>: Send + 'static {
|
pub trait IntoObserverSystem<E: 'static, B: Bundle, M, Out = Result>: Send + 'static {
|
||||||
/// The type of [`System`] that this instance converts into.
|
/// The type of [`System`] that this instance converts into.
|
||||||
type System: ObserverSystem<E, B, Out>;
|
type System: ObserverSystem<E, B, Out>;
|
||||||
|
|
||||||
@ -40,23 +45,150 @@ pub trait IntoObserverSystem<E: 'static, B: Bundle, M, Out = ()>: Send + 'static
|
|||||||
fn into_system(this: Self) -> Self::System;
|
fn into_system(this: Self) -> Self::System;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<
|
impl<E, B, M, Out, S> IntoObserverSystem<E, B, (Fallible, M), Out> for S
|
||||||
S: IntoSystem<Trigger<'static, E, B>, Out, M> + Send + 'static,
|
|
||||||
M,
|
|
||||||
Out,
|
|
||||||
E: 'static,
|
|
||||||
B: Bundle,
|
|
||||||
> IntoObserverSystem<E, B, M, Out> for S
|
|
||||||
where
|
where
|
||||||
|
S: IntoSystem<Trigger<'static, E, B>, Out, M> + Send + 'static,
|
||||||
S::System: ObserverSystem<E, B, Out>,
|
S::System: ObserverSystem<E, B, Out>,
|
||||||
|
E: 'static,
|
||||||
|
B: Bundle,
|
||||||
{
|
{
|
||||||
type System = <S as IntoSystem<Trigger<'static, E, B>, Out, M>>::System;
|
type System = S::System;
|
||||||
|
|
||||||
fn into_system(this: Self) -> Self::System {
|
fn into_system(this: Self) -> Self::System {
|
||||||
IntoSystem::into_system(this)
|
IntoSystem::into_system(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<E, B, M, S> IntoObserverSystem<E, B, (Infallible, M), Result> for S
|
||||||
|
where
|
||||||
|
S: IntoSystem<Trigger<'static, E, B>, (), M> + Send + 'static,
|
||||||
|
S::System: ObserverSystem<E, B, ()>,
|
||||||
|
E: Send + Sync + 'static,
|
||||||
|
B: Bundle,
|
||||||
|
{
|
||||||
|
type System = InfallibleObserverWrapper<E, B, S::System>;
|
||||||
|
|
||||||
|
fn into_system(this: Self) -> Self::System {
|
||||||
|
InfallibleObserverWrapper::new(IntoSystem::into_system(this))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper that converts an observer system that returns `()` into one that returns `Ok(())`.
|
||||||
|
pub struct InfallibleObserverWrapper<E, B, S> {
|
||||||
|
observer: S,
|
||||||
|
_marker: PhantomData<(E, B)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E, B, S> InfallibleObserverWrapper<E, B, S> {
|
||||||
|
/// Create a new `InfallibleObserverWrapper`.
|
||||||
|
pub fn new(observer: S) -> Self {
|
||||||
|
Self {
|
||||||
|
observer,
|
||||||
|
_marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E, B, S> System for InfallibleObserverWrapper<E, B, S>
|
||||||
|
where
|
||||||
|
S: ObserverSystem<E, B, ()>,
|
||||||
|
E: Send + Sync + 'static,
|
||||||
|
B: Bundle,
|
||||||
|
{
|
||||||
|
type In = Trigger<'static, E, B>;
|
||||||
|
type Out = Result;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn name(&self) -> Cow<'static, str> {
|
||||||
|
self.observer.name()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn component_access(&self) -> &Access<ComponentId> {
|
||||||
|
self.observer.component_access()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn archetype_component_access(&self) -> &Access<ArchetypeComponentId> {
|
||||||
|
self.observer.archetype_component_access()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn is_send(&self) -> bool {
|
||||||
|
self.observer.is_send()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn is_exclusive(&self) -> bool {
|
||||||
|
self.observer.is_exclusive()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn has_deferred(&self) -> bool {
|
||||||
|
self.observer.has_deferred()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
unsafe fn run_unsafe(
|
||||||
|
&mut self,
|
||||||
|
input: SystemIn<'_, Self>,
|
||||||
|
world: UnsafeWorldCell,
|
||||||
|
) -> Self::Out {
|
||||||
|
self.observer.run_unsafe(input, world);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn run(&mut self, input: SystemIn<'_, Self>, world: &mut World) -> Self::Out {
|
||||||
|
self.observer.run(input, world);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn apply_deferred(&mut self, world: &mut World) {
|
||||||
|
self.observer.apply_deferred(world);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn queue_deferred(&mut self, world: DeferredWorld) {
|
||||||
|
self.observer.queue_deferred(world);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
unsafe fn validate_param_unsafe(&mut self, world: UnsafeWorldCell) -> bool {
|
||||||
|
self.observer.validate_param_unsafe(world)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn initialize(&mut self, world: &mut World) {
|
||||||
|
self.observer.initialize(world);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) {
|
||||||
|
self.observer.update_archetype_component_access(world);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn check_change_tick(&mut self, change_tick: Tick) {
|
||||||
|
self.observer.check_change_tick(change_tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn get_last_run(&self) -> Tick {
|
||||||
|
self.observer.get_last_run()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn set_last_run(&mut self, last_run: Tick) {
|
||||||
|
self.observer.set_last_run(last_run);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_system_sets(&self) -> Vec<crate::schedule::InternedSystemSet> {
|
||||||
|
self.observer.default_system_sets()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
//! Showcases how fallible systems can be make use of rust's powerful result handling syntax.
|
//! Showcases how fallible systems and observers can make use of Rust's powerful result handling
|
||||||
|
//! syntax.
|
||||||
|
|
||||||
|
use bevy::ecs::world::DeferredWorld;
|
||||||
use bevy::math::sampling::UniformMeshSampler;
|
use bevy::math::sampling::UniformMeshSampler;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
@ -10,6 +12,9 @@ fn main() {
|
|||||||
|
|
||||||
app.add_plugins(DefaultPlugins);
|
app.add_plugins(DefaultPlugins);
|
||||||
|
|
||||||
|
#[cfg(feature = "bevy_mesh_picking_backend")]
|
||||||
|
app.add_plugins(MeshPickingPlugin);
|
||||||
|
|
||||||
// Fallible systems can be used the same way as regular systems. The only difference is they
|
// Fallible systems can be used the same way as regular systems. The only difference is they
|
||||||
// return a `Result<(), Box<dyn Error>>` instead of a `()` (unit) type. Bevy will handle both
|
// return a `Result<(), Box<dyn Error>>` instead of a `()` (unit) type. Bevy will handle both
|
||||||
// types of systems the same way, except for the error handling.
|
// types of systems the same way, except for the error handling.
|
||||||
@ -44,6 +49,9 @@ fn main() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Fallible observers are also sypported.
|
||||||
|
app.add_observer(fallible_observer);
|
||||||
|
|
||||||
// If we run the app, we'll see the following output at startup:
|
// If we run the app, we'll see the following output at startup:
|
||||||
//
|
//
|
||||||
// WARN Encountered an error in system `fallible_systems::failing_system`: "Resource not initialized"
|
// WARN Encountered an error in system `fallible_systems::failing_system`: "Resource not initialized"
|
||||||
@ -53,6 +61,8 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// An example of a system that calls several fallible functions with the question mark operator.
|
/// An example of a system that calls several fallible functions with the question mark operator.
|
||||||
|
///
|
||||||
|
/// See: <https://doc.rust-lang.org/reference/expressions/operator-expr.html#the-question-mark-operator>
|
||||||
fn setup(
|
fn setup(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
@ -118,6 +128,29 @@ fn setup(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Observer systems can also return a `Result`.
|
||||||
|
fn fallible_observer(
|
||||||
|
trigger: Trigger<Pointer<Move>>,
|
||||||
|
mut world: DeferredWorld,
|
||||||
|
mut step: Local<f32>,
|
||||||
|
) -> Result {
|
||||||
|
let mut transform = world
|
||||||
|
.get_mut::<Transform>(trigger.target)
|
||||||
|
.ok_or("No transform found.")?;
|
||||||
|
|
||||||
|
*step = if transform.translation.x > 3. {
|
||||||
|
-0.1
|
||||||
|
} else if transform.translation.x < -3. || *step == 0. {
|
||||||
|
0.1
|
||||||
|
} else {
|
||||||
|
*step
|
||||||
|
};
|
||||||
|
|
||||||
|
transform.translation.x += *step;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
struct UninitializedResource;
|
struct UninitializedResource;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user