owned this note
owned this note
Published
Linked with GitHub
# Permissions and Hooks
## Permissions
Associated type(s) on `Component` which can be used to restrict access to operations. Operations allowed as normal if it's `()`, but if it's something else a value of this type should be passed in.
### MutPerm
- Getting a `&mut` from a `Mut<'_, T>` requires providing a value of `<T as Component>::MutPerm`
- Any other way to get an `&mut` to a component should be gated, I can't think of anything that doesnt go through a `Mut<T>` though
- Code that is generic over component type would now have to write `T: Component<MutPerm = ()>` instead of just `T: Component` if it wanted to mutate the component
- If `InsertPerm = ()` then users can insert a new `T` over the existing component on an entity which is effectively the same as mutating and would bypass `MutPerm`. Every use case of `MutPerm` will likely also want to set `InsertPerm` or add a hook for `on_insert` so this seems like a footgun
#### Indexes Motivation:
Indexes would like to be able to prevent people from mutating components without updating the index. With permissions a crate author can set the `MutPerm` to a type that only they can create. This would allow building API that statically ensures the index is updated after mutation:
```rust
index_resource: ResMut<Index<T>>; // from system param
val: Mut<T>; // from query
index_resource.access_mutably(val, |val: &mut T| {
/* ... */
});
```
A `MutPerm` here is not enough as users could `.insert::<T>` over an existing component of type `T` so we would have to either add an `InsertPerm` or setup an `on_insert` hook.
#### Collections Motivation:
For parent/child hierarchy (or rather general forms of hierarchy / entity relations) we often want to disallow people from mutating the entity in `struct Parent(Entity)` or the list of children in `struct Children(Vec<Entity>)`
If users could write `*parent = *other_parent` they could leave the hierarchy in an invalid state where `Parent` and `Children` components disagree on the shape of the hierarchy.
To solve this we want to set `MutPerm` to some internal type that users cannot create so that we can ensure that the components are only mutated by bevy inside of dedicated commands for mutating the hierarchy.
### InsertPerm
- Calling `world.insert()` requires providing a value of `<T as Component>::InsertPerm` (same is true of `Commands` and `EntityMut`)
- This has the potential to lead to some ugly code i.e.: `.spawn_bundle_with_perm(MyBundle(..), ((), (), (), (), (), MyInsertPerm))` where `MyBundle` contains a bunch of components with only one that requires an `Insert` perm
- Introduce a `Permed<T: Bundle>` type which implements `Bundle` and has an `InsertPerm` of `()`
- Allows libraries to provide nicer APIs for inserting bundles containing components with non `()` insert perms
- The previous code example with `MyBundle` could instead be written like: `.spawn_bundle_with_perm(MyBundle(.., Permed::new(_, MyInsertPerm)))`
- Code that is generic over component type would now have to write `T: Component<InsertPerm = ()>` instead of just `T: Component` if it wanted to insert the component on an entity
#### Motivation:
`MutPerm` is insufficient on its own to detect all mutations of a component as users could insert over an existing component. `InsertPerm` is *a* solution to this problem (another being `on_insert` hooks)
### RemovePerm
- Calling `world.remove()` requires providing a value of `<T as Component>::RemovePerm` (same is true of `Commands` and `EntityMut`)
- Potential issue: this could cause ergonomics to suffer for large bundles, which might need ugly perm types. Could hopefully be solved with enough macros and traits.
- Code that is generic over component type would now have to write `T: Component<RemovePerm = ()>` instead of just `T: Component` if it wanted to remove the component from an entity
#### Motivation:
Haven't come up with a solid use case here but it seems logical to have if we also have `InsertPerm` and `MutPerm`
## Hooks
Functions on `Component` which get called after the relevant operation
* `fn on_insert(Entity, &mut World)`
* `fn on_remove(&mut self, Entity, &mut World)`
Takes mut ref instead of full ownership because `EntityMut::remove` returns the removed value.
* `fn on_remove_from_despawn(&mut self, Entity, &mut World)`
`on_insert` allows us to let `child.insert::<ChildOf>(ChildOf(parent))` work "correctly", automatically adding `child` to the parent's `Children` component (adding the component if required). It could also be used for inserting the other edge of the relation, `parent.insert::<ParentOf>(ParentOf(vec![child1, child2, child3]))` automatically adding `ChildOf(parent)` to all the children entities.
`on_remove` allows us to handle `child.remove::<ChildOf>()`, automatically removing the entity from the parent entity's `Children` component. Can also be used for `parent.remove::<Children>()` automatically removing the `ChildOf` component from all entities in the `Children` component.
`on_remove_from_despawn` allows `.despawn()` to correctly dispose of a parent/child hierarchy instead of requiring a custom `.despawn_recursive` command.
### Bundles:
Bundle operations such as `insert` or `remove` happen all at once and hooks will run after the operation has completed, a subtle interaction here is that hooks are able to observe broken invariants as hooks for other components in the bundle may not have been run yet i.e.: `commands.spawn_bundle((A, Children(...)))`, if `A` has a hook it would be able to observe a `Children` component where the entities do not have the required `ChildOf` component.
## random stuff
I(Boxy) do not think we should have `Insert` or `Remove` perms. Adding perms makes generic code more annoying to write because you have to add an assoc type equality bound (`T: Component<MutPerm = (), InsertPerm = (), RemovePerm = ()>`). Hooks cannot replace the functionality of `MutPerm` and the value of it is high enough that I think this tradeoff is worthwhile. `InsertPerm` and `RemovePerm` on the other hand, hooks _can_ replace the functionality while also allowing APIs to be more consistent with the rest of bevy_ecs (i.e. hooks will allow us to use `parent.remove::<Children>()` instead of creating a custom command for the remove operation `parent.remove_children()`). For this reason `insert`/`remove` permissions should not be added, instead hooks should be used.
It is a little bit sus that hooks would allow a `.remove::<T>` command to effectively run "arbitrary" logic, permissions would require a custom command which more accurately represents what logic the command will end up running.
## alternatives
- TakePerm and more hooks so we can have an on_remove that takes self and a on_take that takes &mut self
^ needs use case
- abandon hooks in favor of relying on Perms and Commands
would still need on_remove_from_despawn at a minimum, but drastically simpler to reason about what could happen
- Batched operations now run hooks after the batch is completed
pros: batched hooks
cons: need to store the entities somewhere, more complicated to think about
- (UNCERTAIN) `fn on_mutate(...?)`