Description
RelationshipSourceCollection::add
and RelationshipSourceCollection::remove
are designed to be fallible. They return a bool
to indicate that. Currently the on_insert
and on_replace
hooks on a RelationshipTarget
make no effort to handle the case where this actually fails. This can result in an invalid state if an entity did not get added.
On line 118, 121 and 159:
bevy/crates/bevy_ecs/src/relationship/mod.rs
Lines 84 to 180 in f47b1c0
Problem
There are two cases (I can think of) for why an implementation of add
would fail:
- The entity already existed in the collection (supported)
- The collection is full
There are also two cases (I can think of) for why an implementation of remove
would fail:
- The entity did not exist in the collection (supported, unreachable with valid state)
- The entity exists in the collection, but cannot be removed (really niche, debatable if needed)
The problem is that respectively only one case is accepted by the current implementation. In the case of add
, both cases exist currently in bevy: The RelationshipSourceCollection
trait is implemented by the EntityHashSet
and by Entity
, the latter acting as a collection with a maximum size of one. When a second entity gets added to the Entity
, currently this case is handled with a panic
.
Solution
Especially the case where the collection has a maximum size is very important, so this should be handled correctly so that it does not result in an invalid state.
To handle the case where the collection is full, there are two possibilities: Panicing, since this should not be allowed, or cancelling the relationship by removing the Relationship
component. But this also requires to correctly identify the two cases. For that I have two solutions:
Solution 1
Change the meaning of the result of RelationshipSourceCollection::add
to indicate the collection is full, since the case where it already existed can simply be handled by not caring about that (like it currently is).
Solution 2
Introduce a result enum to indicate the outcome of the operation, either to indicate all outcomes:
enum CollectionAddResult {
Success,
AlreadyExisting,
NoSpace,
}
Or wrapping it in a Result
:
enum CollectionAddError {
AlreadyExisting,
NoSpace,
}
trait RelationshipSourceCollection {
fn add(&mut self, entity: Entity) -> Result<(), CollectionAddError>
}