Skip to content

When & WhenMut: skip system if Res is missing. #18927

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

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, WhenMut,
},
world::{
EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut,
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_ecs/src/system/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ mod system;
mod system_name;
mod system_param;
mod system_registry;
mod when;

use core::any::TypeId;

Expand All @@ -152,6 +153,7 @@ pub use system::*;
pub use system_name::*;
pub use system_param::*;
pub use system_registry::*;
pub use when::*;

use crate::world::World;

Expand Down
200 changes: 200 additions & 0 deletions crates/bevy_ecs/src/system/when.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
use super::{
ReadOnlySystemParam, Res, ResMut, SystemMeta, SystemParam, SystemParamValidationError,
};
use crate::{
component::{ComponentId, Tick},
prelude::Resource,
storage::ResourceData,
world::{unsafe_world_cell::UnsafeWorldCell, World},
};

/// An alternative to types like [`Res`] that should `skip` instead of panic when they dont exist.
/// Unlike [`Option<Res<T>>`], this will cause the system to be skipped entirely if the resource does not exist.
/// # Example
/// ```
/// # use bevy_ecs::prelude::*;
/// #[derive(Resource)]
/// struct Foo;
///
/// fn skips_if_not_present(res: When<Foo>){}
/// ```
pub struct When<'a, T> {
pub(crate) value: &'a T,
}

impl<T> core::ops::Deref for When<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.value
}
}

// SAFETY: Res only reads a single World resource
unsafe impl<'a, T: Resource> ReadOnlySystemParam for When<'a, T> {}

// SAFETY: Res ComponentId and ArchetypeComponentId access is applied to SystemMeta. If this Res
// conflicts with any prior access, a panic will occur.
unsafe impl<'a, T: Resource> SystemParam for When<'a, T> {
type State = ComponentId;
type Item<'w, 's> = When<'w, T>;

fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State {
Res::<'a, T>::init_state(world, system_meta)
}

#[inline]
unsafe fn validate_param(
&component_id: &Self::State,
_system_meta: &SystemMeta,
world: UnsafeWorldCell,
) -> Result<(), SystemParamValidationError> {
// SAFETY: Read-only access to resource metadata.
if unsafe { world.storages() }
.resources
.get(component_id)
.is_some_and(ResourceData::is_present)
{
Ok(())
} else {
Err(SystemParamValidationError::skipped::<Self>(
"Resource does not exist",
))
}
}

#[inline]
unsafe fn get_param<'w, 's>(
state: &'s mut Self::State,
system_meta: &SystemMeta,
world: UnsafeWorldCell<'w>,
change_tick: Tick,
) -> Self::Item<'w, 's> {
// SAFETY: Res::get_param does not use unsafe code
unsafe {
let value: Res<'w, T> = Res::get_param(state, system_meta, world, change_tick);
When {
value: value.into_inner(),
}
}
}
}

/// An alternative to types like [`ResMut`] that should `skip` instead of panic when they dont exist.
/// Unlike [`Option<ResMut<T>>`], this will cause the system to be skipped entirely if the resource does not exist.
/// # Example
/// ```
/// # use bevy_ecs::prelude::*;
/// #[derive(Resource)]
/// struct Foo;
///
/// fn skips_if_not_present(res: WhenMut<Foo>){}
/// ```
pub struct WhenMut<'a, T> {
pub(crate) value: &'a mut T,
}

impl<T> core::ops::Deref for WhenMut<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.value
}
}

impl<T> core::ops::DerefMut for WhenMut<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.value
}
}

// SAFETY: Res only reads a single World resource
unsafe impl<'a, T: Resource> ReadOnlySystemParam for WhenMut<'a, T> {}

// SAFETY: Res ComponentId and ArchetypeComponentId access is applied to SystemMeta. If this Res
// conflicts with any prior access, a panic will occur.
unsafe impl<'a, T: Resource> SystemParam for WhenMut<'a, T> {
type State = ComponentId;
type Item<'w, 's> = WhenMut<'w, T>;

fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State {
ResMut::<'a, T>::init_state(world, system_meta)
}

#[inline]
unsafe fn validate_param(
&component_id: &Self::State,
_system_meta: &SystemMeta,
world: UnsafeWorldCell,
) -> Result<(), SystemParamValidationError> {
// SAFETY: Read-only access to resource metadata.
if unsafe { world.storages() }
.resources
.get(component_id)
.is_some_and(ResourceData::is_present)
{
Ok(())
} else {
Err(SystemParamValidationError::skipped::<Self>(
"Resource does not exist",
))
}
}

#[inline]
unsafe fn get_param<'w, 's>(
state: &'s mut Self::State,
system_meta: &SystemMeta,
world: UnsafeWorldCell<'w>,
change_tick: Tick,
) -> Self::Item<'w, 's> {
// SAFETY: ResMut::get_param does not use unsafe code
unsafe {
let value: ResMut<'w, T> = ResMut::get_param(state, system_meta, world, change_tick);
WhenMut {
value: value.into_inner(),
}
}
}
}

#[cfg(test)]
mod test {
use crate::prelude::*;

use super::{When, WhenMut};

#[derive(Resource)]
struct Foo;

#[test]
#[should_panic]
fn runs_when_present() {
let mut world = World::new();
world.insert_resource(Foo);
let mut schedule = Schedule::default();
schedule.add_systems(|_res: When<Foo>| panic!("will run"));
schedule.run(&mut world);
}
#[test]
fn skips_when_not_present() {
let mut world = World::new();
let mut schedule = Schedule::default();
schedule.add_systems(|_res: When<Foo>| panic!("will not run"));
schedule.run(&mut world);
}
#[test]
#[should_panic]
fn runs_when_present_mut() {
let mut world = World::new();
world.insert_resource(Foo);
let mut schedule = Schedule::default();
schedule.add_systems(|_res: WhenMut<Foo>| panic!("will run"));
schedule.run(&mut world);
}
#[test]
fn skips_when_not_present_mut() {
let mut world = World::new();
let mut schedule = Schedule::default();
schedule.add_systems(|_res: WhenMut<Foo>| panic!("will not run"));
schedule.run(&mut world);
}
}
16 changes: 16 additions & 0 deletions release-content/release-notes/when-system-param.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
title: When & WhenMut System Params
authors: ["@mrchantey"]
pull_requests: [18927]
---

`Res` & `ResMut` now have two alternatives `When` and `WhenMut`, for when a system should skip instead of panic if the resource is not present.

```rust
// existing options
fn panics_if_not_present(res: Res<Foo>){}
fn runs_even_if_not_present(res: Option<Res<Foo>>){}

// new - skip the system if the resource is missing
fn skips_if_not_present(res: When<Foo>){}
```