Skip to content

Implement SystemCondition for systems returning Result<bool, BevyError> and Result<(), BevyError> #19553

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 4 commits into from
Jun 10, 2025
Merged
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
91 changes: 78 additions & 13 deletions crates/bevy_ecs/src/schedule/condition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,18 @@ pub type BoxedCondition<In = ()> = Box<dyn ReadOnlySystem<In = In, Out = bool>>;

/// A system that determines if one or more scheduled systems should run.
///
/// Implemented for functions and closures that convert into [`System<Out=bool>`](System)
/// with [read-only](crate::system::ReadOnlySystemParam) parameters.
/// `SystemCondition` is sealed and implemented for functions and closures with
/// [read-only](crate::system::ReadOnlySystemParam) parameters that convert into
/// [`System<Out = bool>`](System), [`System<Out = Result<(), BevyError>>`](System) or
/// [`System<Out = Result<bool, BevyError>>`](System).
///
/// `SystemCondition` offers a private method
/// (called by [`run_if`](crate::schedule::IntoScheduleConfigs::run_if) and the provided methods)
/// that converts the implementing system into a condition (system) returning a bool.
/// Depending on the output type of the implementing system:
/// - `bool`: the implementing system is used as the condition;
/// - `Result<(), BevyError>`: the condition returns `true` if and only if the implementing system returns `Ok(())`;
/// - `Result<bool, BevyError>`: the condition returns `true` if and only if the implementing system returns `Ok(true)`.
///
/// # Marker type parameter
///
Expand All @@ -31,7 +41,7 @@ pub type BoxedCondition<In = ()> = Box<dyn ReadOnlySystem<In = In, Out = bool>>;
/// ```
///
/// # Examples
/// A condition that returns true every other time it's called.
/// A condition that returns `true` every other time it's called.
/// ```
/// # use bevy_ecs::prelude::*;
/// fn every_other_time() -> impl SystemCondition<()> {
Expand All @@ -54,7 +64,7 @@ pub type BoxedCondition<In = ()> = Box<dyn ReadOnlySystem<In = In, Out = bool>>;
/// # assert!(!world.resource::<DidRun>().0);
/// ```
///
/// A condition that takes a bool as an input and returns it unchanged.
/// A condition that takes a `bool` as an input and returns it unchanged.
///
/// ```
/// # use bevy_ecs::prelude::*;
Expand All @@ -71,8 +81,30 @@ pub type BoxedCondition<In = ()> = Box<dyn ReadOnlySystem<In = In, Out = bool>>;
/// # world.insert_resource(DidRun(false));
/// # app.run(&mut world);
/// # assert!(world.resource::<DidRun>().0);
pub trait SystemCondition<Marker, In: SystemInput = ()>:
sealed::SystemCondition<Marker, In>
/// ```
///
/// A condition returning a `Result<(), BevyError>`
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # #[derive(Component)] struct Player;
/// fn player_exists(q_player: Query<(), With<Player>>) -> Result {
/// Ok(q_player.single()?)
/// }
///
/// # let mut app = Schedule::default();
/// # #[derive(Resource)] struct DidRun(bool);
/// # fn my_system(mut did_run: ResMut<DidRun>) { did_run.0 = true; }
/// app.add_systems(my_system.run_if(player_exists));
/// # let mut world = World::new();
/// # world.insert_resource(DidRun(false));
/// # app.run(&mut world);
/// # assert!(!world.resource::<DidRun>().0);
/// # world.spawn(Player);
/// # app.run(&mut world);
/// # assert!(world.resource::<DidRun>().0);
pub trait SystemCondition<Marker, In: SystemInput = (), Out = bool>:
sealed::SystemCondition<Marker, In, Out>
{
/// Returns a new run condition that only returns `true`
/// if both this one and the passed `and` return `true`.
Expand Down Expand Up @@ -371,28 +403,61 @@ pub trait SystemCondition<Marker, In: SystemInput = ()>:
}
}

impl<Marker, In: SystemInput, F> SystemCondition<Marker, In> for F where
F: sealed::SystemCondition<Marker, In>
impl<Marker, In: SystemInput, Out, F> SystemCondition<Marker, In, Out> for F where
F: sealed::SystemCondition<Marker, In, Out>
{
}

mod sealed {
use crate::system::{IntoSystem, ReadOnlySystem, SystemInput};
use crate::{
error::BevyError,
system::{IntoSystem, ReadOnlySystem, SystemInput},
};

pub trait SystemCondition<Marker, In: SystemInput>:
IntoSystem<In, bool, Marker, System = Self::ReadOnlySystem>
pub trait SystemCondition<Marker, In: SystemInput, Out>:
IntoSystem<In, Out, Marker, System = Self::ReadOnlySystem>
{
// This associated type is necessary to let the compiler
// know that `Self::System` is `ReadOnlySystem`.
type ReadOnlySystem: ReadOnlySystem<In = In, Out = bool>;
type ReadOnlySystem: ReadOnlySystem<In = In, Out = Out>;

fn into_condition_system(self) -> impl ReadOnlySystem<In = In, Out = bool>;
}

impl<Marker, In: SystemInput, F> SystemCondition<Marker, In> for F
impl<Marker, In: SystemInput, F> SystemCondition<Marker, In, bool> for F
where
F: IntoSystem<In, bool, Marker>,
F::System: ReadOnlySystem,
{
type ReadOnlySystem = F::System;

fn into_condition_system(self) -> impl ReadOnlySystem<In = In, Out = bool> {
IntoSystem::into_system(self)
}
}

impl<Marker, In: SystemInput, F> SystemCondition<Marker, In, Result<(), BevyError>> for F
where
F: IntoSystem<In, Result<(), BevyError>, Marker>,
F::System: ReadOnlySystem,
{
type ReadOnlySystem = F::System;

fn into_condition_system(self) -> impl ReadOnlySystem<In = In, Out = bool> {
IntoSystem::into_system(self.map(|result| result.is_ok()))
}
}

impl<Marker, In: SystemInput, F> SystemCondition<Marker, In, Result<bool, BevyError>> for F
where
F: IntoSystem<In, Result<bool, BevyError>, Marker>,
F::System: ReadOnlySystem,
{
type ReadOnlySystem = F::System;

fn into_condition_system(self) -> impl ReadOnlySystem<In = In, Out = bool> {
IntoSystem::into_system(self.map(|result| matches!(result, Ok(true))))
}
}
}

Expand Down
8 changes: 4 additions & 4 deletions crates/bevy_ecs/src/schedule/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ use crate::{
system::{BoxedSystem, InfallibleSystemWrapper, IntoSystem, ScheduleSystem, System},
};

fn new_condition<M>(condition: impl SystemCondition<M>) -> BoxedCondition {
let condition_system = IntoSystem::into_system(condition);
fn new_condition<M, Out>(condition: impl SystemCondition<M, (), Out>) -> BoxedCondition {
let condition_system = condition.into_condition_system();
assert!(
condition_system.is_send(),
"SystemCondition `{}` accesses `NonSend` resources. This is not currently supported.",
Expand Down Expand Up @@ -447,7 +447,7 @@ pub trait IntoScheduleConfigs<T: Schedulable<Metadata = GraphInfo, GroupMetadata
///
/// Use [`distributive_run_if`](IntoScheduleConfigs::distributive_run_if) if you want the
/// condition to be evaluated for each individual system, right before one is run.
fn run_if<M>(self, condition: impl SystemCondition<M>) -> ScheduleConfigs<T> {
fn run_if<M, Out>(self, condition: impl SystemCondition<M, (), Out>) -> ScheduleConfigs<T> {
self.into_configs().run_if(condition)
}

Expand Down Expand Up @@ -535,7 +535,7 @@ impl<T: Schedulable<Metadata = GraphInfo, GroupMetadata = Chain>> IntoScheduleCo
self
}

fn run_if<M>(mut self, condition: impl SystemCondition<M>) -> ScheduleConfigs<T> {
fn run_if<M, Out>(mut self, condition: impl SystemCondition<M, (), Out>) -> ScheduleConfigs<T> {
self.run_if_dyn(new_condition(condition));
self
}
Expand Down
49 changes: 46 additions & 3 deletions crates/bevy_ecs/src/schedule/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ mod tests {
use alloc::{string::ToString, vec, vec::Vec};
use core::sync::atomic::{AtomicU32, Ordering};

use crate::error::BevyError;
pub use crate::{
prelude::World,
resource::Resource,
Expand All @@ -49,10 +50,10 @@ mod tests {
struct SystemOrder(Vec<u32>);

#[derive(Resource, Default)]
struct RunConditionBool(pub bool);
struct RunConditionBool(bool);

#[derive(Resource, Default)]
struct Counter(pub AtomicU32);
struct Counter(AtomicU32);

fn make_exclusive_system(tag: u32) -> impl FnMut(&mut World) {
move |world| world.resource_mut::<SystemOrder>().0.push(tag)
Expand Down Expand Up @@ -252,12 +253,13 @@ mod tests {
}

mod conditions {

use crate::change_detection::DetectChanges;

use super::*;

#[test]
fn system_with_condition() {
fn system_with_condition_bool() {
let mut world = World::default();
let mut schedule = Schedule::default();

Expand All @@ -276,6 +278,47 @@ mod tests {
assert_eq!(world.resource::<SystemOrder>().0, vec![0]);
}

#[test]
fn system_with_condition_result_unit() {
let mut world = World::default();
let mut schedule = Schedule::default();

world.init_resource::<SystemOrder>();

schedule.add_systems(
make_function_system(0).run_if(|| Err::<(), BevyError>(core::fmt::Error.into())),
);

schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![]);

schedule.add_systems(make_function_system(1).run_if(|| Ok(())));

schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![1]);
}

#[test]
fn system_with_condition_result_bool() {
let mut world = World::default();
let mut schedule = Schedule::default();

world.init_resource::<SystemOrder>();

schedule.add_systems((
make_function_system(0).run_if(|| Err::<bool, BevyError>(core::fmt::Error.into())),
make_function_system(1).run_if(|| Ok(false)),
));

schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![]);

schedule.add_systems(make_function_system(2).run_if(|| Ok(true)));

schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![2]);
}

#[test]
fn systems_with_distributive_condition() {
let mut world = World::default();
Expand Down