EntityCloner remapping sensitive to component registration order.
- Dominant language
- Rust
- Stars
- 48.2k
- Forks
- 4.8k
- Avg merge
- 3d 16h
- Merged PRs (30d)
- 171
Description
## Bevy version and features
`bevy = { version = "0.18", default-features = false, features = ["std", "bevy_log", "multi_threaded"] }`
## What you did
When cloning an entity heirachy with the EntityCloner api, remapping of Entity references on the root entity can fail or succeed depending on the registration order of components on the root entity.
## Minimum example to reproduce
Both tests below clone an entity heirachy, where the root entity holds a reference to a child entity via a `RootLink` component which is annotated with `#[entities]`. The `child` Entity field of `RootLink` will fail or succeed in remapping depending on whether it was added before or after the `ChildOf` component
```
use bevy::{ecs::entity::EntityCloner, prelude::*};
#[derive(Component, Clone, Copy, Debug)]
struct RootLink {
#[entities]
child: Entity,
}
fn clone_root(world: &mut World, root: Entity) -> Entity {
let clone = world.spawn_empty().id();
EntityCloner::build_opt_out(world)
.linked_cloning(true)
.clone_entity(root, clone);
clone
}
#[test]
fn linked_clone_remaps_root_entity_field_when_hierarchy_is_registered_first() {
let mut world = World::new();
let root = world.spawn_empty().id();
let child = world.spawn(ChildOf(root)).id();
world.entity_mut(root).insert(RootLink { child });
let clone = clone_root(&mut world, root);
let cloned_child = world.get::(clone).unwrap().child;
assert_ne!(cloned_child, child);
assert_eq!(
world.get::(cloned_child).map(ChildOf::parent),
Some(clone)
);
}
#[test]
fn linked_clone_leaves_root_entity_field_unmapped_when_root_component_is_registered_first() {
let mut world = World::new();
let root = world
.spawn(RootLink {
child: Entity::PLACEHOLDER,
})
.id();
let child = world.spawn(ChildOf(root)).id();
world.entity_mut(root).insert(RootLink { child });
let clone = clone_root(&mut world, root);
let cloned_child = world.get::(clone).unwrap().child;
assert_eq!(cloned_child, child);
assert_eq!(
world.get::(cloned_child).map(ChildOf::parent),
Some(root)
);
}
//A final test using the add_child api:
#[test]
fn linked_clone_leaves_root_entity_field_unmapped_when_root_component_is_registered_first_api() {
let mut world = World::new();
let mut commands = world.commands();
let child = commands.spawn_empty().id();
let root = commands
.spawn(RootLink { child: child })
.add_child(child)
.id();
world.flush();
let clone = clone_root(&mut world, root);
let cloned_child = world.get::(clone).unwrap().child;
assert_eq!(cloned_child, child);
assert_eq!(
world.get::(cloned_child).map(ChildOf::parent),
Some(root)
);
}
```
## Discussion on discord:
https://discord.com/channels/691052431525675048/1502218838392049725
With possible cause:
https://discord.com/channels/691052431525675048/1502218838392049725/1502430680225812680
> **eugineerd:** I think this happens because when hierarchy is cloned Children adds an entity mapping to the mapper used by EntityCloner. When a component is cloned, it's map_entities function is called, which uses this mapper to obtain correct entity to use, but if there isn't an entity mapped then it returns the original Entity value.
>
> So in this case since Children is added to the entity first, it's also the first to be cloned, which sets correct entity value in the mapper. When ActionGroup is cloned, it can find the correct entity since it has been set by Children before.
>
> So yeah, this is definitely a bug. #[relationship(..)] components aren't affected by it, so it makes sense why it wasn't encountered before I guess.
Contributor guide
Research direction
Start with ecs::entity::EntityCloner and the linked-cloning path, then run the three reproduction tests from the issue with the stated Bevy features. Trace how hierarchy components and #[entities] fields update the entity mapper during cloning. Done means RootLink references are remapped correctly regardless of component registration order, including the add_child case, with regression tests passing.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- game-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100