Create a When system param wrapper for skipping systems that fail validation (#18765)

# Objective

Create a `When` system param wrapper for skipping systems that fail
validation.

Currently, the `Single` and `Populated` parameters cause systems to skip
when they fail validation, while the `Res` family causes systems to
error. Generalize this so that any fallible parameter can be used either
to skip a system or to raise an error. A parameter used directly will
always raise an error, and a parameter wrapped in `When<P>` will always
cause the system to be silently skipped.

~~Note that this changes the behavior for `Single` and `Populated`. The
current behavior will be available using `When<Single>` and
`When<Populated>`.~~

Fixes #18516

## Solution

Create a `When` system param wrapper that wraps an inner parameter and
converts all validation errors to `skipped`.

~~Change the behavior of `Single` and `Populated` to fail by default.~~

~~Replace in-engine use of `Single` with `When<Single>`. I updated the
`fallible_systems` example, but not all of the others. The other
examples I looked at appeared to always have one matching entity, and it
seemed more clear to use the simpler type in those cases.~~

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Zachary Harrold <zac@harrold.com.au>
Co-authored-by: François Mockers <mockersf@gmail.com>
This commit is contained in:
Chris Russell 2025-05-04 04:41:42 -04:00 committed by GitHub
parent 56405890f2
commit d28e4908ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 124 additions and 4 deletions

View File

@ -94,7 +94,7 @@ pub mod prelude {
Command, Commands, Deferred, EntityCommand, EntityCommands, In, InMut, InRef, Command, Commands, Deferred, EntityCommand, EntityCommands, In, InMut, InRef,
IntoSystem, Local, NonSend, NonSendMut, ParamSet, Populated, Query, ReadOnlySystem, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Populated, Query, ReadOnlySystem,
Res, ResMut, Single, System, SystemIn, SystemInput, SystemParamBuilder, Res, ResMut, Single, System, SystemIn, SystemInput, SystemParamBuilder,
SystemParamFunction, SystemParamFunction, When,
}, },
world::{ world::{
EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut,

View File

@ -7,7 +7,7 @@ use crate::{
query::{QueryData, QueryFilter, QueryState}, query::{QueryData, QueryFilter, QueryState},
resource::Resource, resource::Resource,
system::{ system::{
DynSystemParam, DynSystemParamState, Local, ParamSet, Query, SystemMeta, SystemParam, DynSystemParam, DynSystemParamState, Local, ParamSet, Query, SystemMeta, SystemParam, When,
}, },
world::{ world::{
FilteredResources, FilteredResourcesBuilder, FilteredResourcesMut, FilteredResources, FilteredResourcesBuilder, FilteredResourcesMut,
@ -710,6 +710,19 @@ unsafe impl<'w, 's, T: FnOnce(&mut FilteredResourcesMutBuilder)>
} }
} }
/// A [`SystemParamBuilder`] for a [`When`].
#[derive(Clone)]
pub struct WhenBuilder<T>(T);
// SAFETY: `WhenBuilder<B>` builds a state that is valid for `P`, and any state valid for `P` is valid for `When<P>`
unsafe impl<P: SystemParam, B: SystemParamBuilder<P>> SystemParamBuilder<When<P>>
for WhenBuilder<B>
{
fn build(self, world: &mut World, meta: &mut SystemMeta) -> <When<P> as SystemParam>::State {
self.0.build(world, meta)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -1916,6 +1916,112 @@ unsafe impl SystemParam for SystemChangeTick {
} }
} }
/// A [`SystemParam`] that wraps another parameter and causes its system to skip instead of failing when the parameter is invalid.
///
/// # Example
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # #[derive(Resource)]
/// # struct SomeResource;
/// // This system will fail if `SomeResource` is not present.
/// fn fails_on_missing_resource(res: Res<SomeResource>) {}
///
/// // This system will skip without error if `SomeResource` is not present.
/// fn skips_on_missing_resource(res: When<Res<SomeResource>>) {
/// // The inner parameter is available using `Deref`
/// let some_resource: &SomeResource = &res;
/// }
/// # bevy_ecs::system::assert_is_system(skips_on_missing_resource);
/// ```
#[derive(Debug)]
pub struct When<T>(pub T);
impl<T> When<T> {
/// Returns the inner `T`.
///
/// The inner value is `pub`, so you can also obtain it by destructuring the parameter:
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # #[derive(Resource)]
/// # struct SomeResource;
/// fn skips_on_missing_resource(When(res): When<Res<SomeResource>>) {
/// let some_resource: Res<SomeResource> = res;
/// }
/// # bevy_ecs::system::assert_is_system(skips_on_missing_resource);
/// ```
pub fn into_inner(self) -> T {
self.0
}
}
impl<T> Deref for When<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for When<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
// SAFETY: Delegates to `T`, which ensures the safety requirements are met
unsafe impl<T: SystemParam> SystemParam for When<T> {
type State = T::State;
type Item<'world, 'state> = When<T::Item<'world, 'state>>;
fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State {
T::init_state(world, system_meta)
}
#[inline]
unsafe fn validate_param(
state: &Self::State,
system_meta: &SystemMeta,
world: UnsafeWorldCell,
) -> Result<(), SystemParamValidationError> {
T::validate_param(state, system_meta, world).map_err(|mut e| {
e.skipped = true;
e
})
}
#[inline]
unsafe fn get_param<'world, 'state>(
state: &'state mut Self::State,
system_meta: &SystemMeta,
world: UnsafeWorldCell<'world>,
change_tick: Tick,
) -> Self::Item<'world, 'state> {
When(T::get_param(state, system_meta, world, change_tick))
}
unsafe fn new_archetype(
state: &mut Self::State,
archetype: &Archetype,
system_meta: &mut SystemMeta,
) {
// SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`.
unsafe { T::new_archetype(state, archetype, system_meta) };
}
fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) {
T::apply(state, system_meta, world);
}
fn queue(state: &mut Self::State, system_meta: &SystemMeta, world: DeferredWorld) {
T::queue(state, system_meta, world);
}
}
// SAFETY: Delegates to `T`, which ensures the safety requirements are met
unsafe impl<T: ReadOnlySystemParam> ReadOnlySystemParam for When<T> {}
// SAFETY: When initialized with `init_state`, `get_param` returns an empty `Vec` and does no access. // SAFETY: When initialized with `init_state`, `get_param` returns an empty `Vec` and does no access.
// Therefore, `init_state` trivially registers all access, and no accesses can conflict. // Therefore, `init_state` trivially registers all access, and no accesses can conflict.
// Note that the safety requirements for non-empty `Vec`s are handled by the `SystemParamBuilder` impl that builds them. // Note that the safety requirements for non-empty `Vec`s are handled by the `SystemParamBuilder` impl that builds them.
@ -2699,11 +2805,12 @@ pub struct SystemParamValidationError {
/// By default, this will result in a panic. See [`crate::error`] for more information. /// By default, this will result in a panic. See [`crate::error`] for more information.
/// ///
/// This is the default behavior, and is suitable for system params that should *always* be valid, /// This is the default behavior, and is suitable for system params that should *always* be valid,
/// either because sensible fallback behavior exists (like [`Query`] or because /// either because sensible fallback behavior exists (like [`Query`]) or because
/// failures in validation should be considered a bug in the user's logic that must be immediately addressed (like [`Res`]). /// failures in validation should be considered a bug in the user's logic that must be immediately addressed (like [`Res`]).
/// ///
/// If `true`, the system should be skipped. /// If `true`, the system should be skipped.
/// This is suitable for system params that are intended to only operate in certain application states, such as [`Single`]. /// This is set by wrapping the system param in [`When`],
/// and indicates that the system is intended to only operate in certain application states.
pub skipped: bool, pub skipped: bool,
/// A message describing the validation error. /// A message describing the validation error.