Validation and recovery solution for Bevy
https://github.com/Zeenobit/moonshine_check.git
Validation and recovery solution for Bevy.
This crate is deprecated in favor of component hooks and required components.
Because of component requirements, there should never be a need to manually check for component dependencies or to check for [Kind] correctness.
#[derive(Component)]
#[require(Name)] // Person will always have `Name`. Period.
struct Person;
To validate entities on component addition, you may you use the on_add hook and perform any check or repair you want:
use bevy::ecs::component::ComponentId;
use bevy::world::DeferredWorld;
#[derive(Component)]
#[require(Name)]
#[component(on_add = on_person_add)]
struct Person;
fn on_person_add(mut world: DeferredWorld, entity: Entity, _: ComponentId) {
// ... Validate/Repair/Replace/Upgrade components ...
}
A common source of bugs in Bevy applications is invalid assumptions about the state of the world. Typically, this results in queries that "miss" their target entities due to query mismatch:
use bevy::prelude::*;
#[derive(Component)]
struct Person;
let mut app = App::new();
app.add_systems(Update, (bad_system, unsafe_system));
fn bad_system(mut commands: Commands) {
commands.spawn().insert(Person); // Bug: Name is missing.
}
// This system will silently skip over any `Person` entities without `Name`:
fn unsafe_system(people: Query<(&Person, &Name)>) {
for (person, name) in people.iter() {
println!("{:?}", name);
}
}
While this example is trivial, this problem gets much worse for interdependent systems that must rely on some invariants to be able to function correctly together.
There are various solutions to this problem. Some solutions include:
Expect<T> in system queries.Kind semantics to enforce requirements between system boundaries.Expect<T> can lead to lots of crashes, which nobody likes.Kind semantics are only enforced at the time of query and require manual validation if needed.use bevy::prelude::*;
use moonshine_check::prelude::*;
#[derive(Component)]
struct Person;
let mut app = App::new();
// Check for `Person` entities without `Name` and purge them (despawn recursively):
app.check::<Person, Without<Name>>(purge());
app.add_systems(Update, (bad_system, safe_system));
fn bad_system(mut commands: Commands) {
// Because of the check, this entity will be purged before the next frame:
commands.spawn().insert(Person); // Bug: Name is missing.
}
// This system will never skip a `Person` ever again!
fn safe_system(people: Query<(&Person, &Name)>) {
for (person, name) in people.iter() {
println!("{:?}", name);
}
}
The check method is used to add a new check to the application:
use bevy::prelude::*;
use moonshine_check::prelude::*;
#[derive(Component)]
struct A;
#[derive(Component)]
struct B;
let mut app = App::new();
// ...
app.check::<A, Without<B>>(purge());
This function takes a [Kind], a QueryFilter, and a [Policy].
Internally, it adds a system which applies the given policy to all new entities of the given kind which match the given filter.
Entities that do NOT match the query are considered Valid.
Once an entity is checked, it will not be checked again unless manually requested (see [check_again]).
There are 4 possible ways to recover from an invalid entity:
invalid()This policy marks the entity as invalid and generates an error message.
This, combined with [Valid] allows you to define fault-tolerant systems:
use bevy::prelude::*;
use moonshine_check::prelude::*;
#[derive(Component)]
struct A;
#[derive(Component)]
struct B;
let mut app = App::new();
// ...
app.check::<A, Without<B>>(invalid());
app.world_mut().spawn(A); // Bug!
// Pass `Valid` to your system query:
fn safe_system(query: Query<(Entity, &A), Valid>, b: Query<&B>) {
for (entity, a) in query.iter() {
// Safe:
let b = b.get(entity).unwrap();
}
}
purge()This policy despawns the entity with all of its children and generates an error message. This prevents it from being queried by any system.
use bevy::prelude::*;
use moonshine_check::prelude::*;
#[derive(Component)]
struct A;
#[derive(Component)]
struct B;
let mut app = App::new();
// ...
app.check::<A, Without<B>>(purge());
app.world_mut().spawn(A); // Bug!
// No need for `Valid`:
fn safe_system(query: Query<(Entity, &A)>, b: Query<&B>) {
for (entity, a) in query.iter() {
// Safe:
let b = b.get(entity).unwrap();
}
}
panic()This policy generates an error messages and stops program execution immediately.
This is mainly useful for debugging and should be avoided in a production environment. It is equivalent to burning the whole world down because your coffee is too hot! โ๐ฅ
use bevy::prelude::*;
use moonshine_check::prelude::*;
#[derive(Component)]
struct A;
#[derive(Component)]
struct B;
let mut app = App::new();
// ...
app.check::<A, Without<B>>(panic());
app.world_mut().spawn(A); // Bug!
// No need for `Valid`:
fn safe_system(query: Query<(Entity, &A)>, b: Query<&B>) {
// Doesn't matter, we'll never get here...
unreachable!();
}
repair(f)This policy generates a warning message and attempts to repair the entity with a given [Fixer].
This is useful when you can automatically restore the invalid entity into a valid state. For example, it's just missing a random marker component. There is no need to make a big fuss about it.
This policy is also useful for backwards compatibility, as it may be used to automatically upgrade saved entities to a new version.
โจ This crate is specifically designed to work withmoonshine-save. All check systems are inserted afterLoadSystem::Loadto ensure loaded data is always valid. ๐
use bevy::prelude::*;
use moonshine_check::prelude::*;
#[derive(Component)]
struct A;
#[derive(Component)]
struct B;
let mut app = App::new();
// ...
app.check::<A, Without<B>>(repair(|entity, commands| {
// It's fine, we can fix it! :D
commands.entity(entity).insert(B);
}));
app.world_mut().spawn(A); // Bug!
// No need for `Valid`:
fn safe_system(query: Query<(Entity, &A)>, b: Query<&B>) {
for (entity, a) in query.iter() {
// Safe:
let b = b.get(entity).unwrap();
}
}
Checks are not free. Each check adds a new system to your application, which may impact your performance.
Avoid using checks excessively. Instead, try to check broad assumptions that are critical for application correctness. For example, you may use checks to ensure all [Kind] casts are valid:
use bevy::prelude::*;
use moonshine_check::prelude::*;
use moonshine_kind::prelude::*;
#[derive(Component)]
struct Fruit;
#[derive(Component)]
struct Apple;
#[derive(Bundle)]
struct AppleBundle {
fruit: Fruit,
apple: Apple,
}
kind!(Apple is Fruit); // Enforced by the bundle
let mut app = App::new();
// ...
app.check::<Apple, Without<Fruit>>(purge()); // Encorced by checking
Remember that once an entity is checked, it will not be checked again unless explicitly required. This means if any of the invariants are changed after the entity was checked, it will not be detected.
You can force an entity to be checked again by calling check_again:
``rust,ignore
commands.entity(entity).check_again();
%%CODEBLOCK10%%rust,ignore
app.check::<A, Without<B>>(repair(|entity, commands| {
// ...
// Did we actually fix it? Not sure? Check again!
commands.entity(entity).check_again();
}));
%%CODEBLOCK11%%rust,ignore
#[cfg(feature = "debug_checks")]
app.check::<A, (Without<B>, Without<B2>)>(panic());
%%CODEBLOCK12%%rust,ignore
app.check::<A, With<B>>(repair(|entity, commands| {
// Update B -> B2
commands.entity(entity).remove::<B>();
commands.entity(entity).insert(B2::default());
}));
`
[Kind]:https://docs.rs/moonshine-kind/latest/moonshine_kind/trait.Kind.html
[Policy]:https://docs.rs/moonshine-check/latest/moonshine_check/struct.Policy.html
[Valid]:https://docs.rs/moonshine-check/latest/moonshine_check/struct.Valid.html
[Fixer]:https://docs.rs/moonshine-check/latest/moonshine_check/trait.Fix.html
[checkagain`]:https://docs.rs/moonshine-check/latest/moonshinecheck/trait.CheckAgain.html#method.check_again