Fix custom relations panics with parent/child relations (#19341)

# Objective

Fixes #18905

## Solution

`world.commands().entity(target_entity).queue(command)` calls
`commands.with_entity` without an error handler, instead queue on
`Commands` with an error handler

## Testing

Added unit test

Co-authored-by: Heart <>
This commit is contained in:
HeartofPhos 2025-05-27 23:05:31 +02:00 committed by GitHub
parent e981bb8902
commit 131f99de23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -158,19 +158,21 @@ pub trait Relationship: Component + Sized {
{
relationship_target.collection_mut_risky().remove(entity);
if relationship_target.len() == 0 {
if let Ok(mut entity) = world.commands().get_entity(target_entity) {
let command = |mut entity: EntityWorldMut| {
// this "remove" operation must check emptiness because in the event that an identical
// relationship is inserted on top, this despawn would result in the removal of that identical
// relationship ... not what we want!
entity.queue(|mut entity: EntityWorldMut| {
if entity
.get::<Self::RelationshipTarget>()
.is_some_and(RelationshipTarget::is_empty)
{
entity.remove::<Self::RelationshipTarget>();
}
});
}
if entity
.get::<Self::RelationshipTarget>()
.is_some_and(RelationshipTarget::is_empty)
{
entity.remove::<Self::RelationshipTarget>();
}
};
world
.commands()
.queue(command.with_entity(target_entity).handle_error_with(ignore));
}
}
}
@ -424,4 +426,63 @@ mod tests {
// No assert necessary, looking to make sure compilation works with the macros
}
#[test]
fn parent_child_relationship_with_custom_relationship() {
use crate::prelude::ChildOf;
#[derive(Component)]
#[relationship(relationship_target = RelTarget)]
struct Rel(Entity);
#[derive(Component)]
#[relationship_target(relationship = Rel)]
struct RelTarget(Entity);
let mut world = World::new();
// Rel on Parent
// Despawn Parent
let mut commands = world.commands();
let child = commands.spawn_empty().id();
let parent = commands.spawn(Rel(child)).add_child(child).id();
commands.entity(parent).despawn();
world.flush();
assert!(world.get_entity(child).is_err());
assert!(world.get_entity(parent).is_err());
// Rel on Parent
// Despawn Child
let mut commands = world.commands();
let child = commands.spawn_empty().id();
let parent = commands.spawn(Rel(child)).add_child(child).id();
commands.entity(child).despawn();
world.flush();
assert!(world.get_entity(child).is_err());
assert!(!world.entity(parent).contains::<Rel>());
// Rel on Child
// Despawn Parent
let mut commands = world.commands();
let parent = commands.spawn_empty().id();
let child = commands.spawn((ChildOf(parent), Rel(parent))).id();
commands.entity(parent).despawn();
world.flush();
assert!(world.get_entity(child).is_err());
assert!(world.get_entity(parent).is_err());
// Rel on Child
// Despawn Child
let mut commands = world.commands();
let parent = commands.spawn_empty().id();
let child = commands.spawn((ChildOf(parent), Rel(parent))).id();
commands.entity(child).despawn();
world.flush();
assert!(world.get_entity(child).is_err());
assert!(!world.entity(parent).contains::<RelTarget>());
}
}