This commit is contained in:
Chris Russell 2025-07-18 09:43:44 -04:00 committed by GitHub
commit c6383356ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 48 additions and 26 deletions

View File

@ -318,7 +318,7 @@ mod tests {
use crate::{ use crate::{
prelude::{Component, In, IntoSystem, Resource, Schedule}, prelude::{Component, In, IntoSystem, Resource, Schedule},
schedule::ExecutorKind, schedule::ExecutorKind,
system::{Populated, Res, ResMut, Single}, system::{Populated, Res, ResMut, Single, When},
world::World, world::World,
}; };
@ -341,12 +341,12 @@ mod tests {
#[derive(Resource, Default)] #[derive(Resource, Default)]
struct Counter(u8); struct Counter(u8);
fn set_single_state(mut _single: Single<&TestComponent>, mut state: ResMut<TestState>) { fn set_single_state(mut _single: When<Single<&TestComponent>>, mut state: ResMut<TestState>) {
state.single_ran = true; state.single_ran = true;
} }
fn set_populated_state( fn set_populated_state(
mut _populated: Populated<&TestComponent>, mut _populated: When<Populated<&TestComponent>>,
mut state: ResMut<TestState>, mut state: ResMut<TestState>,
) { ) {
state.populated_ran = true; state.populated_ran = true;
@ -418,7 +418,7 @@ mod tests {
#[test] #[test]
fn piped_systems_first_system_skipped() { fn piped_systems_first_system_skipped() {
// This system should be skipped when run due to no matching entity // This system should be skipped when run due to no matching entity
fn pipe_out(_single: Single<&TestComponent>) -> u8 { fn pipe_out(_single: When<Single<&TestComponent>>) -> u8 {
42 42
} }
@ -446,7 +446,11 @@ mod tests {
} }
// This system should be skipped when run due to no matching entity // This system should be skipped when run due to no matching entity
fn pipe_in(_input: In<u8>, _single: Single<&TestComponent>, mut counter: ResMut<Counter>) { fn pipe_in(
_input: In<u8>,
_single: When<Single<&TestComponent>>,
mut counter: ResMut<Counter>,
) {
counter.0 += 1; counter.0 += 1;
} }
@ -499,7 +503,7 @@ mod tests {
#[test] #[test]
fn piped_system_skip_and_panic() { fn piped_system_skip_and_panic() {
// This system should be skipped when run due to no matching entity // This system should be skipped when run due to no matching entity
fn pipe_out(_single: Single<&TestComponent>) -> u8 { fn pipe_out(_single: When<Single<&TestComponent>>) -> u8 {
42 42
} }
@ -523,7 +527,7 @@ mod tests {
} }
// This system should be skipped when run due to no matching entity // This system should be skipped when run due to no matching entity
fn pipe_in(_input: In<u8>, _single: Single<&TestComponent>) {} fn pipe_in(_input: In<u8>, _single: When<Single<&TestComponent>>) {}
let mut world = World::new(); let mut world = World::new();
let mut schedule = Schedule::default(); let mut schedule = Schedule::default();
@ -555,13 +559,17 @@ mod tests {
fn piped_system_skip_and_skip() { fn piped_system_skip_and_skip() {
// This system should be skipped when run due to no matching entity // This system should be skipped when run due to no matching entity
fn pipe_out(_single: Single<&TestComponent>, mut counter: ResMut<Counter>) -> u8 { fn pipe_out(_single: When<Single<&TestComponent>>, mut counter: ResMut<Counter>) -> u8 {
counter.0 += 1; counter.0 += 1;
42 42
} }
// This system should be skipped when run due to no matching entity // This system should be skipped when run due to no matching entity
fn pipe_in(_input: In<u8>, _single: Single<&TestComponent>, mut counter: ResMut<Counter>) { fn pipe_in(
_input: In<u8>,
_single: When<Single<&TestComponent>>,
mut counter: ResMut<Counter>,
) {
counter.0 += 1; counter.0 += 1;
} }

View File

@ -2581,9 +2581,11 @@ impl<'w, 'q, Q: QueryData, F: QueryFilter> From<&'q mut Query<'w, '_, Q, F>>
/// [System parameter] that provides access to single entity's components, much like [`Query::single`]/[`Query::single_mut`]. /// [System parameter] that provides access to single entity's components, much like [`Query::single`]/[`Query::single_mut`].
/// ///
/// This [`SystemParam`](crate::system::SystemParam) fails validation if zero or more than one matching entity exists. /// This [`SystemParam`](crate::system::SystemParam) fails validation if zero or more than one matching entity exists.
/// This will cause the system to be skipped, according to the rules laid out in [`SystemParamValidationError`](crate::system::SystemParamValidationError). /// This will cause the system to fail, according to the rules laid out in [`SystemParamValidationError`](crate::system::SystemParamValidationError).
/// ///
/// Use [`Option<Single<D, F>>`] instead if zero or one matching entities can exist. /// Use [`Option<Single<D, F>>`] instead to run the system but get `None` when there are zero or multiple matching entities.
///
/// Use [`When<Single<D, F>>`](crate::system::When) instead to skip the system when there are zero or multiple matching entities.
/// ///
/// See [`Query`] for more details. /// See [`Query`] for more details.
/// ///
@ -2632,7 +2634,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Single<'w, 's, D, F> {
/// [System parameter] that works very much like [`Query`] except it always contains at least one matching entity. /// [System parameter] that works very much like [`Query`] except it always contains at least one matching entity.
/// ///
/// This [`SystemParam`](crate::system::SystemParam) fails validation if no matching entities exist. /// This [`SystemParam`](crate::system::SystemParam) fails validation if no matching entities exist.
/// This will cause the system to be skipped, according to the rules laid out in [`SystemParamValidationError`](crate::system::SystemParamValidationError). /// This will cause the system to fail, according to the rules laid out in [`SystemParamValidationError`](crate::system::SystemParamValidationError).
///
/// Use [`When<Populated<D, F>>`](crate::system::When) instead to skip the system when there are zero matching entities.
/// ///
/// Much like [`Query::is_empty`] the worst case runtime will be `O(n)` where `n` is the number of *potential* matches. /// Much like [`Query::is_empty`] the worst case runtime will be `O(n)` where `n` is the number of *potential* matches.
/// This can be notably expensive for queries that rely on non-archetypal filters such as [`Added`](crate::query::Added), /// This can be notably expensive for queries that rely on non-archetypal filters such as [`Added`](crate::query::Added),

View File

@ -448,10 +448,10 @@ unsafe impl<'a, 'b, D: QueryData + 'static, F: QueryFilter + 'static> SystemPara
match query.single_inner() { match query.single_inner() {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(QuerySingleError::NoEntities(_)) => Err( Err(QuerySingleError::NoEntities(_)) => Err(
SystemParamValidationError::skipped::<Self>("No matching entities"), SystemParamValidationError::invalid::<Self>("No matching entities"),
), ),
Err(QuerySingleError::MultipleEntities(_)) => Err( Err(QuerySingleError::MultipleEntities(_)) => Err(
SystemParamValidationError::skipped::<Self>("Multiple matching entities"), SystemParamValidationError::invalid::<Self>("Multiple matching entities"),
), ),
} }
} }
@ -509,7 +509,7 @@ unsafe impl<D: QueryData + 'static, F: QueryFilter + 'static> SystemParam
state.query_unchecked_with_ticks(world, system_meta.last_run, world.change_tick()) state.query_unchecked_with_ticks(world, system_meta.last_run, world.change_tick())
}; };
if query.is_empty() { if query.is_empty() {
Err(SystemParamValidationError::skipped::<Self>( Err(SystemParamValidationError::invalid::<Self>(
"No matching entities", "No matching entities",
)) ))
} else { } else {
@ -1836,6 +1836,8 @@ unsafe impl<T: ReadOnlySystemParam> ReadOnlySystemParam for Result<T, SystemPara
/// A [`SystemParam`] that wraps another parameter and causes its system to skip instead of failing when the parameter is invalid. /// A [`SystemParam`] that wraps another parameter and causes its system to skip instead of failing when the parameter is invalid.
/// ///
/// This is especially useful with [`Single`] and [`Populated`].
///
/// # Example /// # Example
/// ///
/// ``` /// ```
@ -2803,12 +2805,6 @@ pub struct SystemParamValidationError {
} }
impl SystemParamValidationError { impl SystemParamValidationError {
/// Constructs a `SystemParamValidationError` that skips the system.
/// The parameter name is initialized to the type name of `T`, so a `SystemParam` should usually pass `Self`.
pub fn skipped<T>(message: impl Into<Cow<'static, str>>) -> Self {
Self::new::<T>(true, message, Cow::Borrowed(""))
}
/// Constructs a `SystemParamValidationError` for an invalid parameter that should be treated as an error. /// Constructs a `SystemParamValidationError` for an invalid parameter that should be treated as an error.
/// The parameter name is initialized to the type name of `T`, so a `SystemParam` should usually pass `Self`. /// The parameter name is initialized to the type name of `T`, so a `SystemParam` should usually pass `Self`.
pub fn invalid<T>(message: impl Into<Cow<'static, str>>) -> Self { pub fn invalid<T>(message: impl Into<Cow<'static, str>>) -> Self {

View File

@ -243,10 +243,10 @@ pub type InputFocusSet = InputFocusSystems;
/// If no entity is focused, sets the focus to the primary window, if any. /// If no entity is focused, sets the focus to the primary window, if any.
pub fn set_initial_focus( pub fn set_initial_focus(
mut input_focus: ResMut<InputFocus>, mut input_focus: ResMut<InputFocus>,
window: Single<Entity, With<PrimaryWindow>>, window: When<Single<Entity, With<PrimaryWindow>>>,
) { ) {
if input_focus.0.is_none() { if input_focus.0.is_none() {
input_focus.0 = Some(*window); input_focus.0 = Some(**window);
} }
} }

View File

@ -119,8 +119,8 @@ fn user_input(
// System that moves the enemies in a circle. // System that moves the enemies in a circle.
// Only runs if there are enemies, due to the `Populated` parameter. // Only runs if there are enemies, due to the `Populated` parameter.
fn move_targets(mut enemies: Populated<(&mut Transform, &mut Enemy)>, time: Res<Time>) { fn move_targets(mut enemies: When<Populated<(&mut Transform, &mut Enemy)>>, time: Res<Time>) {
for (mut transform, mut target) in &mut *enemies { for (mut transform, mut target) in &mut **enemies {
target.rotation += target.rotation_speed * time.delta_secs(); target.rotation += target.rotation_speed * time.delta_secs();
transform.rotation = Quat::from_rotation_z(target.rotation); transform.rotation = Quat::from_rotation_z(target.rotation);
let offset = transform.right() * target.radius; let offset = transform.right() * target.radius;
@ -160,4 +160,4 @@ fn track_targets(
/// This system always fails param validation, because we never /// This system always fails param validation, because we never
/// create an entity with both [`Player`] and [`Enemy`] components. /// create an entity with both [`Player`] and [`Enemy`] components.
fn do_nothing_fail_validation(_: Single<(), (With<Player>, With<Enemy>)>) {} fn do_nothing_fail_validation(_: When<Single<(), (With<Player>, With<Enemy>)>>) {}

View File

@ -0,0 +1,14 @@
---
title: "`Single` and `Populated` now fail instead of skipping"
pull_requests: [19489, 18765]
---
`Single<D, F>` and `Populated<D, F>` now cause systems to fail instead of skip.
The introduction of `When` makes it possible to skip systems for any invalid parameter, such as `When<Res<R>>`.
The change to the behavior of `Single` and `Populated` keeps them consistent with other parameters,
and makes it possible to use them as assertions instead of only as run conditions.
Replace `Single<D, F>` with `When<Single<D, F>>`
and `Populated<D, F>` with `When<Populated<D, F>>`
to restore the old behavior.