๐Ÿ“ฆ Zeenobit / moonshine_check

Validation and recovery solution for Bevy

โ˜… 2 stars โ‘‚ 0 forks ๐Ÿ‘ 2 watching โš–๏ธ MIT License
๐Ÿ“ฅ Clone https://github.com/Zeenobit/moonshine_check.git
HTTPS git clone https://github.com/Zeenobit/moonshine_check.git
SSH git clone git@github.com:Zeenobit/moonshine_check.git
CLI gh repo clone Zeenobit/moonshine_check
Zeenobit Zeenobit Update `moonshine-util` version a2b1aee 8 months ago ๐Ÿ“ History
๐Ÿ“‚ main View all commits โ†’
๐Ÿ“ src
๐Ÿ“„ .gitattributes
๐Ÿ“„ .gitignore
๐Ÿ“„ Cargo.toml
๐Ÿ“„ LICENSE
๐Ÿ“„ README.md
๐Ÿ“„ README.md

โœ… Moonshine Check

crates.io downloads docs.rs license stars

Validation and recovery solution for Bevy.

โš ๏ธ Deprecated

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 ...
}

Overview

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:

  • Hiding components inside bundles to ensure they are always inserted together.
  • Using Expect<T> in system queries.
  • Using Kind semantics to enforce requirements between system boundaries.
While these solutions are all valid, they each have flaws:
  • Bundles cannot overlap, and there is no guarantee that the bundle components will never be removed.
  • Excessive use of 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.
This crate offers a "last resort" solution to fully guarantee invariants in your application. It provides a standard way to check entities for correctness and allows you to handle failures gracefully:

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);
    }
}

Usage

Check

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]).

Policies

There are 4 possible ways to recover from an invalid entity:

1. 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();
    }
}

2. 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();
    }
}

3. 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!();
}

4. 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 with moonshine-save. All check systems are inserted after LoadSystem::Load to 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();
    }
}

Guidelines and Limitations

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