use std::collections::HashMap;
use std::hash::Hash;
use bevy_ecs::prelude::*;
use bevy_ecs::schedule::{IntoScheduleConfigs, Schedule};
use bevy_ecs::system::ScheduleSystem;
pub trait States: Clone + Eq + Hash + Send + Sync + 'static {}
impl<T> States for T where T: Clone + Eq + Hash + Send + Sync + 'static {}
#[derive(Resource)]
pub struct State<S: States>(S);
impl<S: States> State<S> {
pub fn new(value: S) -> Self {
Self(value)
}
pub fn get(&self) -> &S {
&self.0
}
}
#[derive(Resource)]
pub struct NextState<S: States>(Option<S>);
impl<S: States> NextState<S> {
pub fn new() -> Self {
Self(None)
}
pub fn set(&mut self, next: S) {
self.0 = Some(next);
}
pub fn pending(&self) -> Option<&S> {
self.0.as_ref()
}
fn take(&mut self) -> Option<S> {
self.0.take()
}
}
impl<S: States> Default for NextState<S> {
fn default() -> Self {
Self(None)
}
}
#[derive(Resource)]
pub(crate) struct StateSchedules<S: States> {
on_enter: HashMap<S, Schedule>,
on_exit: HashMap<S, Schedule>,
on_transition: HashMap<(S, S), Schedule>,
}
impl<S: States> Default for StateSchedules<S> {
fn default() -> Self {
Self {
on_enter: HashMap::new(),
on_exit: HashMap::new(),
on_transition: HashMap::new(),
}
}
}
pub const MAX_CHAIN: u32 = 8;
pub(crate) fn apply<S: States>(world: &mut World) {
for _ in 0..MAX_CHAIN {
let Some(next) = world.resource_mut::<NextState<S>>().take() else {
break;
};
let current = world.resource::<State<S>>().get().clone();
if next == current {
continue;
}
world.resource_scope(|world, mut schedules: Mut<StateSchedules<S>>| {
if let Some(schedule) = schedules.on_exit.get_mut(¤t) {
schedule.run(world);
}
world.resource_mut::<State<S>>().0 = next.clone();
if let Some(schedule) = schedules.on_transition.get_mut(&(current, next.clone())) {
schedule.run(world);
}
if let Some(schedule) = schedules.on_enter.get_mut(&next) {
schedule.run(world);
}
});
}
}
pub fn in_state<S: States>(state: S) -> impl FnMut(Res<State<S>>) -> bool + Clone + Send + Sync + 'static {
move |current: Res<State<S>>| current.get() == &state
}
pub(crate) fn init_schedules<S: States>(world: &mut World) {
world.insert_resource(StateSchedules::<S>::default());
}
pub(crate) fn add_enter<S, M>(
world: &mut World,
state: S,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) where
S: States,
{
world
.resource_mut::<StateSchedules<S>>()
.on_enter
.entry(state)
.or_default()
.add_systems(systems);
}
pub(crate) fn add_exit<S, M>(
world: &mut World,
state: S,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) where
S: States,
{
world
.resource_mut::<StateSchedules<S>>()
.on_exit
.entry(state)
.or_default()
.add_systems(systems);
}
pub(crate) fn add_transition<S, M>(
world: &mut World,
exited: S,
entered: S,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) where
S: States,
{
world
.resource_mut::<StateSchedules<S>>()
.on_transition
.entry((exited, entered))
.or_default()
.add_systems(systems);
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
enum Mode {
Menu,
Play,
Paused,
}
#[test]
fn transition_runs_exit_then_enter_in_order() {
use std::sync::{Arc, Mutex};
let log: Arc<Mutex<Vec<&'static str>>> = Arc::new(Mutex::new(Vec::new()));
let mut sim = crate::Sim::with_default_step();
sim.add_state(Mode::Menu);
let (a, b, c) = (log.clone(), log.clone(), log.clone());
sim.on_exit(Mode::Menu, move || a.lock().unwrap().push("exit:menu"));
sim.on_enter(Mode::Play, move || b.lock().unwrap().push("enter:play"));
sim.on_transition(Mode::Menu, Mode::Play, move || {
c.lock().unwrap().push("transition:menu->play")
});
sim.set_state(Mode::Play);
sim.tick();
assert_eq!(
*log.lock().unwrap(),
["exit:menu", "transition:menu->play", "enter:play"]
);
assert_eq!(*sim.world.resource::<State<Mode>>().get(), Mode::Play);
}
#[test]
fn exit_sees_old_state_and_enter_sees_new_state() {
use std::sync::{Arc, Mutex};
let seen: Arc<Mutex<Vec<Mode>>> = Arc::new(Mutex::new(Vec::new()));
let mut sim = crate::Sim::with_default_step();
sim.add_state(Mode::Menu);
let e = seen.clone();
sim.on_exit(Mode::Menu, move |s: Res<State<Mode>>| e.lock().unwrap().push(*s.get()));
let n = seen.clone();
sim.on_enter(Mode::Play, move |s: Res<State<Mode>>| n.lock().unwrap().push(*s.get()));
sim.set_state(Mode::Play);
sim.tick();
assert_eq!(*seen.lock().unwrap(), [Mode::Menu, Mode::Play]);
}
#[test]
fn a_transition_requested_on_enter_chains_on_the_same_step() {
let mut sim = crate::Sim::with_default_step();
sim.add_state(Mode::Menu);
sim.on_enter(Mode::Play, |mut next: ResMut<NextState<Mode>>| {
next.set(Mode::Paused)
});
sim.set_state(Mode::Play);
sim.tick();
assert_eq!(*sim.world.resource::<State<Mode>>().get(), Mode::Paused);
}
#[test]
fn self_triggering_transition_is_bounded() {
let mut sim = crate::Sim::with_default_step();
sim.add_state(Mode::Menu);
sim.on_enter(Mode::Play, |mut next: ResMut<NextState<Mode>>| {
next.set(Mode::Play)
});
sim.set_state(Mode::Play);
sim.tick();
assert_eq!(*sim.world.resource::<State<Mode>>().get(), Mode::Play);
assert!(sim.world.resource::<NextState<Mode>>().pending().is_none());
}
#[test]
fn set_to_current_state_runs_no_schedules() {
use std::sync::{Arc, Mutex};
let fired = Arc::new(Mutex::new(false));
let mut sim = crate::Sim::with_default_step();
sim.add_state(Mode::Menu);
let f = fired.clone();
sim.on_enter(Mode::Menu, move || *f.lock().unwrap() = true);
sim.set_state(Mode::Menu);
sim.tick();
assert!(!*fired.lock().unwrap());
}
#[test]
fn in_state_condition_gates_systems() {
use std::sync::atomic::{AtomicU32, Ordering};
static HITS: AtomicU32 = AtomicU32::new(0);
let mut sim = crate::Sim::with_default_step();
sim.add_state(Mode::Menu);
sim.add_chained_systems(
(|| {
HITS.fetch_add(1, Ordering::SeqCst);
})
.run_if(in_state(Mode::Menu)),
);
sim.tick();
assert_eq!(HITS.load(Ordering::SeqCst), 1);
sim.set_state(Mode::Play);
sim.tick();
assert_eq!(HITS.load(Ordering::SeqCst), 1, "gated out after switching");
}
}