diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 8a07cdc8e1b92..25b4ec229153e 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -151,10 +151,12 @@ mod tests { component::{Component, ComponentId, RequiredComponents, RequiredComponentsError}, entity::{Entity, EntityMapper}, entity_disabling::DefaultQueryFilters, + lifecycle::HookContext, prelude::Or, query::{Added, Changed, FilteredAccess, QueryFilter, With, Without}, resource::Resource, - world::{EntityMut, EntityRef, Mut, World}, + system::SystemId, + world::{DeferredWorld, EntityMut, EntityRef, Mut, World}, }; use alloc::{ string::{String, ToString}, @@ -2776,4 +2778,45 @@ mod tests { fn custom_clone(_source: &SourceComponent, _ctx: &mut ComponentCloneCtx) {} } + + #[test] + fn register_system_cached_on_deferred_world() { + let mut world = World::new(); + + let first_entity = world.spawn(ComponentWithOnAddHook).id(); + let second_entity = world.spawn(ComponentWithOnAddHook).id(); + + let first_system = world + .entity(first_entity) + .get::() + .unwrap() + .0; + let second_system = world + .entity(second_entity) + .get::() + .unwrap() + .0; + + assert_eq!(first_system, second_system); + + #[derive(Component)] + #[component(on_add = Self::on_add)] + struct ComponentWithOnAddHook; + + impl ComponentWithOnAddHook { + fn on_add(mut world: DeferredWorld, context: HookContext) { + let system_id = + world.register_system_cached(Self::the_system_that_will_be_registered); + world + .commands() + .entity(context.entity) + .insert(RegisteredSystem(system_id)); + } + + fn the_system_that_will_be_registered() {} + } + + #[derive(Component)] + struct RegisteredSystem(SystemId); + } } diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 64f1fa409f9aa..df9e6cd67fe43 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -14,7 +14,7 @@ use crate::{ query::{QueryData, QueryFilter}, relationship::RelationshipHookMode, resource::Resource, - system::{Commands, Query}, + system::{CachedSystemId, Commands, IntoSystem, Query, SystemId, SystemInput}, traversal::Traversal, world::{error::EntityMutableFetchError, EntityFetcher, WorldEntityFetch}, }; @@ -890,4 +890,42 @@ impl<'w> DeferredWorld<'w> { pub(crate) fn as_unsafe_world_cell(&mut self) -> UnsafeWorldCell { self.world } + + /// Registers a system or returns its cached [`SystemId`]. + /// + /// If you want to run the system immediately and you don't need its `SystemId`, see + /// [`Commands::run_system_cached`]. + /// + /// The first time this function is called for a particular system, it will register it and + /// store its [`SystemId`] in a [`CachedSystemId`] resource for later. If you would rather + /// manage the `SystemId` yourself, or register multiple copies of the same system, use + /// [`Commands::register_system`] instead. + /// + /// # Limitations + /// + /// If this function is called twice for the same system before commands are executed, then it will register the system twice. + /// + /// This function only accepts ZST (zero-sized) systems to guarantee that any two systems of + /// the same type must be equal. This means that closures that capture the environment, and + /// function pointers, are not accepted. + /// + /// If you want to access values from the environment within a system, consider passing them in + /// as inputs via [`Commands::run_system_cached_with`]. If that's not an option, consider + /// [`Commands::register_system`] instead. + pub fn register_system_cached(&mut self, system: S) -> SystemId + where + I: SystemInput + Send + 'static, + O: Send + 'static, + S: IntoSystem + 'static, + { + match self.get_resource::>() { + Some(cached_system) => SystemId::from_entity(cached_system.entity), + None => { + let mut commands = self.commands(); + let system_id = commands.register_system(system); + commands.insert_resource(CachedSystemId::::new(system_id)); + system_id + } + } + } }