From bc00178b596f637391d556f1e3cdbfc620f3bebe Mon Sep 17 00:00:00 2001 From: eugineerd <70062110+eugineerd@users.noreply.github.com> Date: Fri, 30 May 2025 22:28:53 +0300 Subject: [PATCH] Fix `EntityCloner` replacing required components. (#19326) # Objective Fix #19324 ## Solution `EntityCloner` replaces required components when filtering. This is unexpected when comparing with the way the rest of bevy handles required components. This PR separates required components from explicit components when filtering in `EntityClonerBuilder`. ## Testing Added a regression test for this case. --- crates/bevy_ecs/src/entity/clone_entities.rs | 67 +++++++++++++++++--- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/crates/bevy_ecs/src/entity/clone_entities.rs b/crates/bevy_ecs/src/entity/clone_entities.rs index 3337894066..dd6cb360e9 100644 --- a/crates/bevy_ecs/src/entity/clone_entities.rs +++ b/crates/bevy_ecs/src/entity/clone_entities.rs @@ -5,6 +5,7 @@ use bumpalo::Bump; use core::any::TypeId; use crate::{ + archetype::Archetype, bundle::Bundle, component::{Component, ComponentCloneBehavior, ComponentCloneFn, ComponentId, ComponentInfo}, entity::{hash_map::EntityHashMap, Entities, Entity, EntityMapper}, @@ -346,6 +347,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { pub struct EntityCloner { filter_allows_components: bool, filter: HashSet, + filter_required: HashSet, clone_behavior_overrides: HashMap, move_components: bool, linked_cloning: bool, @@ -362,6 +364,7 @@ impl Default for EntityCloner { linked_cloning: false, default_clone_fn: ComponentCloneBehavior::global_default_fn(), filter: Default::default(), + filter_required: Default::default(), clone_behavior_overrides: Default::default(), clone_queue: Default::default(), deferred_commands: Default::default(), @@ -465,6 +468,12 @@ impl EntityCloner { { let world = world.as_unsafe_world_cell(); let source_entity = world.get_entity(source).expect("Source entity must exist"); + let target_archetype = (!self.filter_required.is_empty()).then(|| { + world + .get_entity(target) + .expect("Target entity must exist") + .archetype() + }); #[cfg(feature = "bevy_reflect")] // SAFETY: we have unique access to `world`, nothing else accesses the registry at this moment, and we clone @@ -481,7 +490,7 @@ impl EntityCloner { bundle_scratch = BundleScratch::with_capacity(archetype.component_count()); for component in archetype.components() { - if !self.is_cloning_allowed(&component) { + if !self.is_cloning_allowed(&component, target_archetype) { continue; } @@ -605,9 +614,19 @@ impl EntityCloner { target } - fn is_cloning_allowed(&self, component: &ComponentId) -> bool { - (self.filter_allows_components && self.filter.contains(component)) - || (!self.filter_allows_components && !self.filter.contains(component)) + fn is_cloning_allowed( + &self, + component: &ComponentId, + target_archetype: Option<&Archetype>, + ) -> bool { + if self.filter_allows_components { + self.filter.contains(component) + || target_archetype.is_some_and(|archetype| { + !archetype.contains(*component) && self.filter_required.contains(component) + }) + } else { + !self.filter.contains(component) && !self.filter_required.contains(component) + } } } @@ -809,9 +828,9 @@ impl<'w> EntityClonerBuilder<'w> { if let Some(info) = self.world.components().get_info(id) { for required_id in info.required_components().iter_ids() { if self.entity_cloner.filter_allows_components { - self.entity_cloner.filter.insert(required_id); + self.entity_cloner.filter_required.insert(required_id); } else { - self.entity_cloner.filter.remove(&required_id); + self.entity_cloner.filter_required.remove(&required_id); } } } @@ -829,9 +848,9 @@ impl<'w> EntityClonerBuilder<'w> { if let Some(info) = self.world.components().get_info(id) { for required_id in info.required_components().iter_ids() { if self.entity_cloner.filter_allows_components { - self.entity_cloner.filter.remove(&required_id); + self.entity_cloner.filter_required.remove(&required_id); } else { - self.entity_cloner.filter.insert(required_id); + self.entity_cloner.filter_required.insert(required_id); } } } @@ -1406,4 +1425,36 @@ mod tests { ); assert!(world.resource::().0); } + + #[test] + fn cloning_with_required_components_preserves_existing() { + #[derive(Component, Clone, PartialEq, Debug, Default)] + #[require(B(5))] + struct A; + + #[derive(Component, Clone, PartialEq, Debug)] + struct B(u32); + + let mut world = World::default(); + + let e = world.spawn((A, B(0))).id(); + let e_clone = world.spawn(B(1)).id(); + + EntityCloner::build(&mut world) + .deny_all() + .allow::() + .clone_entity(e, e_clone); + + assert_eq!(world.entity(e_clone).get::(), Some(&A)); + assert_eq!(world.entity(e_clone).get::(), Some(&B(1))); + + let e_clone2 = world.spawn(B(2)).id(); + + EntityCloner::build(&mut world) + .allow_all() + .deny::() + .clone_entity(e, e_clone2); + + assert_eq!(world.entity(e_clone2).get::(), Some(&B(2))); + } }