#![warn(
clippy::pedantic,
clippy::clone_on_ref_ptr,
clippy::create_dir,
clippy::filetype_is_file,
clippy::fn_to_numeric_cast_any,
clippy::if_then_some_else_none,
missing_docs,
clippy::missing_docs_in_private_items,
missing_copy_implementations,
missing_debug_implementations,
clippy::missing_const_for_fn,
clippy::mixed_read_write_in_expression,
clippy::panic,
clippy::partial_pub_fields,
clippy::same_name_method,
clippy::str_to_string,
clippy::suspicious_xor_used_as_pow,
clippy::try_err,
clippy::unneeded_field_pattern,
clippy::use_debug,
clippy::verbose_file_reads,
clippy::expect_used
)]
#![deny(
clippy::unwrap_used,
clippy::unreachable,
clippy::unimplemented,
clippy::todo,
clippy::dbg_macro,
clippy::error_impl_error,
clippy::exit,
clippy::panic_in_result_fn,
clippy::tests_outside_test_module
)]
use bevy::prelude::*;
pub trait AppExtension {
#[cfg(feature = "gc")]
fn add_gc_state<S: States>(&mut self);
#[cfg(feature = "gc")]
fn add_gc_resource<S: States, R: Resource>(&mut self, state: S);
#[cfg(feature = "nested")]
fn add_substate<P: States, S: States + Default>(&mut self, parent: P, sub: S);
}
#[derive(Component, Debug)]
pub struct Gc<S: States>(pub S);
impl AppExtension for App {
fn add_gc_state<S: States>(&mut self) {
self.add_systems(
PostUpdate,
|mut commands: Commands, state: Res<State<S>>, query: Query<(Entity, &Gc<S>)>| {
if state.is_changed() {
for (entity, expected_state) in query.iter() {
if state.get() != &expected_state.0 {
commands.entity(entity).despawn_recursive();
}
}
}
},
);
}
fn add_gc_resource<S: States, R: Resource>(&mut self, expected_state: S) {
self.add_systems(
PostUpdate,
move |mut commands: Commands, state: Res<State<S>>| {
if state.is_changed() {
if state.get() != &expected_state {
commands.remove_resource::<R>();
}
}
},
);
}
#[cfg(feature = "nested")]
fn add_substate<P: States, S: States + Default>(&mut self, parent: P, sub: S) {
let parent_clone = parent.clone();
self.add_systems(
Update,
(
move |mut commands: Commands, current_parent: Res<State<P>>| {
if current_parent.is_changed() && current_parent.get() == &parent {
commands.insert_resource(NextState(Some(sub.clone())));
}
},
move |mut commands: Commands, current_parent: Res<State<P>>| {
if current_parent.is_changed() && current_parent.get() != &parent_clone {
commands.insert_resource(NextState(Some(S::default())));
}
},
),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(States, Default, Clone, Hash, Eq, PartialEq, Debug)]
enum StatesA {
#[default]
A,
B,
}
#[derive(States, Default, Clone, Hash, Eq, PartialEq, Debug)]
enum StatesB {
#[default]
A,
B,
}
#[test]
fn gc_resource() {
let mut app = App::new();
app.init_state::<StatesA>();
app.add_gc_state::<StatesA>();
let id = app.world.spawn(Gc(StatesA::A)).id();
app.update();
assert!(app.world.get::<Gc<StatesA>>(id).is_some());
app.insert_resource(NextState(Some(StatesA::B)));
app.update();
assert!(app.world.get::<Gc<StatesA>>(id).is_none());
}
#[derive(Resource)]
struct Res;
#[test]
fn gc() {
let mut app = App::new();
app.init_state::<StatesA>();
app.add_gc_resource::<_, Res>(StatesA::A);
app.insert_resource(Res);
app.update();
assert!(app.world.get_resource::<Res>().is_some());
app.insert_resource(NextState(Some(StatesA::B)));
app.update();
assert!(app.world.get_resource::<Res>().is_none());
}
#[test]
fn nested() {
let mut app = App::new();
app.init_state::<StatesA>();
app.init_state::<StatesB>();
app.add_substate(StatesA::B, StatesB::B);
app.update();
assert_eq!(
app.world.get_resource::<State<StatesB>>().unwrap().get(),
&StatesB::A
);
app.insert_resource(NextState(Some(StatesA::B)));
app.update();
app.update();
assert_eq!(
app.world.get_resource::<State<StatesB>>().unwrap().get(),
&StatesB::B
);
app.insert_resource(NextState(Some(StatesA::A)));
app.update();
app.update();
assert_eq!(
app.world.get_resource::<State<StatesB>>().unwrap().get(),
&StatesB::A
);
}
}