Recursive run_system (#18076)

# Objective

Fixes #18030

## Solution

When running a one-shot system, requeue the system's command queue onto
the world's command queue, then execute the later.

If running the entire command queue of the world is undesired, I could
add a new method to `RawCommandQueue` to only apply part of it.

## Testing

See the new test.

---

## Showcase

```rust
#[derive(Resource)]
pub struct Test {
    id: SystemId,
    counter: u32,
}

let mut world = World::new();
let id = world.register_system(|mut commands: Commands, mut test: ResMut<Test>| {
    print!("{:?} ", test.counter);
    test.counter -= 1;
    if test.counter > 0 {
        commands.run_system(test.id);
    }
});
world.insert_resource(Test { id, counter: 5 });
world.run_system(id).unwrap();
```

```
5 4 3 2 1 
```
This commit is contained in:
SpecificProtagonist 2025-03-10 22:38:36 +01:00 committed by GitHub
parent 79e7f8ae0c
commit 54247bcf86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 63 additions and 17 deletions

View File

@ -117,7 +117,11 @@ where
panic!("Cannot run exclusive systems with a shared World reference");
}
fn run(&mut self, input: SystemIn<'_, Self>, world: &mut World) -> Self::Out {
fn run_without_applying_deferred(
&mut self,
input: SystemIn<'_, Self>,
world: &mut World,
) -> Self::Out {
world.last_change_tick_scope(self.system_meta.last_run, |world| {
#[cfg(feature = "trace")]
let _span_guard = self.system_meta.system_span.enter();

View File

@ -83,14 +83,25 @@ pub trait System: Send + Sync + 'static {
///
/// [`run_readonly`]: ReadOnlySystem::run_readonly
fn run(&mut self, input: SystemIn<'_, Self>, world: &mut World) -> Self::Out {
let ret = self.run_without_applying_deferred(input, world);
self.apply_deferred(world);
ret
}
/// Runs the system with the given input in the world.
///
/// [`run_readonly`]: ReadOnlySystem::run_readonly
fn run_without_applying_deferred(
&mut self,
input: SystemIn<'_, Self>,
world: &mut World,
) -> Self::Out {
let world_cell = world.as_unsafe_world_cell();
self.update_archetype_component_access(world_cell);
// SAFETY:
// - We have exclusive access to the entire world.
// - `update_archetype_component_access` has been called.
let ret = unsafe { self.run_unsafe(input, world_cell) };
self.apply_deferred(world);
ret
unsafe { self.run_unsafe(input, world_cell) }
}
/// Applies any [`Deferred`](crate::system::Deferred) system parameters (or other system buffers) of this system to the world.

View File

@ -213,12 +213,10 @@ impl World {
/// This is different from [`RunSystemOnce::run_system_once`](crate::system::RunSystemOnce::run_system_once),
/// because it keeps local state between calls and change detection works correctly.
///
/// Also runs any queued-up commands.
///
/// In order to run a chained system with an input, use [`World::run_system_with`] instead.
///
/// # Limitations
///
/// - Stored systems cannot be recursive, they cannot call themselves through [`Commands::run_system`](crate::system::Commands).
///
/// # Examples
///
/// ## Running a system
@ -305,9 +303,7 @@ impl World {
/// Before running a system, it must first be registered.
/// The method [`World::register_system`] stores a given system and returns a [`SystemId`].
///
/// # Limitations
///
/// - Stored systems cannot be recursive, they cannot call themselves through [`Commands::run_system`](crate::system::Commands).
/// Also runs any queued-up commands.
///
/// # Examples
///
@ -336,12 +332,12 @@ impl World {
I: SystemInput + 'static,
O: 'static,
{
// lookup
// Lookup
let mut entity = self
.get_entity_mut(id.entity)
.map_err(|_| RegisteredSystemError::SystemIdNotRegistered(id))?;
// take ownership of system trait object
// Take ownership of system trait object
let RegisteredSystem {
mut initialized,
mut system,
@ -349,25 +345,32 @@ impl World {
.take::<RegisteredSystem<I, O>>()
.ok_or(RegisteredSystemError::Recursive(id))?;
// run the system
// Run the system
if !initialized {
system.initialize(self);
initialized = true;
}
let result = if system.validate_param(self) {
Ok(system.run(input, self))
// Wait to run the commands until the system is available again.
// This is needed so the systems can recursively run themselves.
let ret = system.run_without_applying_deferred(input, self);
system.queue_deferred(self.into());
Ok(ret)
} else {
Err(RegisteredSystemError::InvalidParams(id))
};
// return ownership of system trait object (if entity still exists)
// Return ownership of system trait object (if entity still exists)
if let Ok(mut entity) = self.get_entity_mut(id.entity) {
entity.insert::<RegisteredSystem<I, O>>(RegisteredSystem {
initialized,
system,
});
}
// Run any commands enqueued by the system
self.flush();
result
}
@ -509,8 +512,13 @@ impl<I: SystemInput, O> core::fmt::Debug for RegisteredSystemError<I, O> {
}
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
use core::cell::Cell;
use bevy_utils::default;
use crate::{prelude::*, system::SystemId};
#[derive(Resource, Default, PartialEq, Debug)]
struct Counter(u8);
@ -863,4 +871,27 @@ mod tests {
Err(RegisteredSystemError::InvalidParams(_))
));
}
#[test]
fn run_system_recursive() {
std::thread_local! {
static INVOCATIONS_LEFT: Cell<i32> = const { Cell::new(3) };
static SYSTEM_ID: Cell<Option<SystemId>> = default();
}
fn system(mut commands: Commands) {
let count = INVOCATIONS_LEFT.get() - 1;
INVOCATIONS_LEFT.set(count);
if count > 0 {
commands.run_system(SYSTEM_ID.get().unwrap());
}
}
let mut world = World::new();
let id = world.register_system(system);
SYSTEM_ID.set(Some(id));
world.run_system(id).unwrap();
assert_eq!(INVOCATIONS_LEFT.get(), 0);
}
}