Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 4, 2025
2 changes: 1 addition & 1 deletion crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ pub mod prelude {
Command, Commands, Deferred, EntityCommand, EntityCommands, In, InMut, InRef,
IntoSystem, Local, NonSend, NonSendMut, ParamSet, Populated, Query, ReadOnlySystem,
Res, ResMut, Single, System, SystemIn, SystemInput, SystemParamBuilder,
SystemParamFunction,
SystemParamFunction, When,
},
world::{
EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut,
Expand Down
15 changes: 14 additions & 1 deletion crates/bevy_ecs/src/system/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
query::{QueryData, QueryFilter, QueryState},
resource::Resource,
system::{
DynSystemParam, DynSystemParamState, Local, ParamSet, Query, SystemMeta, SystemParam,
DynSystemParam, DynSystemParamState, Local, ParamSet, Query, SystemMeta, SystemParam, When,
},
world::{
FilteredResources, FilteredResourcesBuilder, FilteredResourcesMut,
Expand Down Expand Up @@ -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)]
mod tests {
use crate::{
Expand Down
111 changes: 109 additions & 2 deletions crates/bevy_ecs/src/system/system_param.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
// 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.
Expand Down Expand Up @@ -2699,11 +2805,12 @@ pub struct SystemParamValidationError {
/// 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,
/// 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`]).
///
/// 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,

/// A message describing the validation error.
Expand Down