use crate::Schedule;
use crate::dynamic::ResourceHost;
use crate::system_param::{EventHost, IntoSystem};
use std::marker::PhantomData;
pub struct State<S> {
pub current: S,
pub initialized: bool,
}
pub struct NextState<S> {
pub pending: Option<S>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StateTransition<S> {
pub before: Option<S>,
pub after: S,
}
pub fn insert_state<S, W>(host: &mut W, initial: S)
where
S: Copy + PartialEq + Send + Sync + 'static,
W: ResourceHost,
{
let map = host.resource_map_mut();
map.insert(State {
current: initial,
initialized: false,
});
map.insert(NextState::<S> { pending: None });
}
pub fn current_state<S, W>(host: &W) -> S
where
S: Copy + PartialEq + Send + Sync + 'static,
W: ResourceHost,
{
host.resource_map()
.get::<State<S>>()
.unwrap_or_else(|| {
panic!(
"no state of type {} was inserted; call insert_state first",
std::any::type_name::<S>()
)
})
.current
}
pub fn in_state<S, W>(host: &W, state: S) -> bool
where
S: Copy + PartialEq + Send + Sync + 'static,
W: ResourceHost,
{
current_state::<S, W>(host) == state
}
pub fn next_state<S, W>(host: &mut W, state: S)
where
S: Copy + PartialEq + Send + Sync + 'static,
W: ResourceHost,
{
host.resource_map_mut()
.get_mut::<NextState<S>>()
.unwrap_or_else(|| {
panic!(
"no state of type {} was inserted; call insert_state first",
std::any::type_name::<S>()
)
})
.pending = Some(state);
}
pub fn apply_state_transition<S, W>(host: &mut W)
where
S: Copy + PartialEq + Send + Sync + 'static,
W: ResourceHost + EventHost,
{
let transition = {
let map = host.resource_map_mut();
let Some(state) = map.get::<State<S>>() else {
return;
};
let current = state.current;
let initialized = state.initialized;
if !initialized {
map.get_mut::<State<S>>()
.expect("state present above")
.initialized = true;
if let Some(next) = map.get_mut::<NextState<S>>()
&& next.pending == Some(current)
{
next.pending = None;
}
Some(StateTransition {
before: None,
after: current,
})
} else {
let pending = map.get::<NextState<S>>().and_then(|next| next.pending);
match pending {
Some(next) => {
if let Some(next_state) = map.get_mut::<NextState<S>>() {
next_state.pending = None;
}
if next != current {
map.get_mut::<State<S>>()
.expect("state present above")
.current = next;
Some(StateTransition {
before: Some(current),
after: next,
})
} else {
None
}
}
None => None,
}
}
};
if let Some(transition) = transition {
host.event_bus_mut().send::<StateTransition<S>>(transition);
}
}
pub struct SingleMarker<Marker>(PhantomData<fn() -> Marker>);
pub struct GroupMarker<Marker>(PhantomData<fn() -> Marker>);
pub trait IntoGroupRunner<W, Marker> {
fn into_group_runner(self) -> impl FnMut(&mut W) + Send + 'static;
}
impl<W, Marker, F> IntoGroupRunner<W, SingleMarker<Marker>> for F
where
F: IntoSystem<W, Marker>,
{
fn into_group_runner(self) -> impl FnMut(&mut W) + Send + 'static {
self.into_runner()
}
}
macro_rules! impl_group_runner_tuple {
($($system:ident $marker:ident $runner:ident),+) => {
impl<W, $($system, $marker,)+> IntoGroupRunner<W, GroupMarker<($($marker,)+)>>
for ($($system,)+)
where
$($system: IntoSystem<W, $marker>,)+
{
fn into_group_runner(self) -> impl FnMut(&mut W) + Send + 'static {
let ($($runner,)+) = self;
$(let mut $runner = $runner.into_runner();)+
move |world: &mut W| {
$($runner(world);)+
}
}
}
};
}
impl_group_runner_tuple!(S0 M0 r0);
impl_group_runner_tuple!(S0 M0 r0, S1 M1 r1);
impl_group_runner_tuple!(S0 M0 r0, S1 M1 r1, S2 M2 r2);
impl_group_runner_tuple!(S0 M0 r0, S1 M1 r1, S2 M2 r2, S3 M3 r3);
impl_group_runner_tuple!(S0 M0 r0, S1 M1 r1, S2 M2 r2, S3 M3 r3, S4 M4 r4);
impl_group_runner_tuple!(S0 M0 r0, S1 M1 r1, S2 M2 r2, S3 M3 r3, S4 M4 r4, S5 M5 r5);
impl_group_runner_tuple!(S0 M0 r0, S1 M1 r1, S2 M2 r2, S3 M3 r3, S4 M4 r4, S5 M5 r5, S6 M6 r6);
impl_group_runner_tuple!(
S0 M0 r0, S1 M1 r1, S2 M2 r2, S3 M3 r3, S4 M4 r4, S5 M5 r5, S6 M6 r6, S7 M7 r7
);
impl_group_runner_tuple!(
S0 M0 r0, S1 M1 r1, S2 M2 r2, S3 M3 r3, S4 M4 r4, S5 M5 r5, S6 M6 r6, S7 M7 r7, S8 M8 r8
);
impl_group_runner_tuple!(
S0 M0 r0, S1 M1 r1, S2 M2 r2, S3 M3 r3, S4 M4 r4, S5 M5 r5, S6 M6 r6, S7 M7 r7, S8 M8 r8,
S9 M9 r9
);
impl_group_runner_tuple!(
S0 M0 r0, S1 M1 r1, S2 M2 r2, S3 M3 r3, S4 M4 r4, S5 M5 r5, S6 M6 r6, S7 M7 r7, S8 M8 r8,
S9 M9 r9, S10 M10 r10
);
impl_group_runner_tuple!(
S0 M0 r0, S1 M1 r1, S2 M2 r2, S3 M3 r3, S4 M4 r4, S5 M5 r5, S6 M6 r6, S7 M7 r7, S8 M8 r8,
S9 M9 r9, S10 M10 r10, S11 M11 r11
);
impl_group_runner_tuple!(
S0 M0 r0, S1 M1 r1, S2 M2 r2, S3 M3 r3, S4 M4 r4, S5 M5 r5, S6 M6 r6, S7 M7 r7, S8 M8 r8,
S9 M9 r9, S10 M10 r10, S11 M11 r11, S12 M12 r12
);
impl_group_runner_tuple!(
S0 M0 r0, S1 M1 r1, S2 M2 r2, S3 M3 r3, S4 M4 r4, S5 M5 r5, S6 M6 r6, S7 M7 r7, S8 M8 r8,
S9 M9 r9, S10 M10 r10, S11 M11 r11, S12 M12 r12, S13 M13 r13
);
impl_group_runner_tuple!(
S0 M0 r0, S1 M1 r1, S2 M2 r2, S3 M3 r3, S4 M4 r4, S5 M5 r5, S6 M6 r6, S7 M7 r7, S8 M8 r8,
S9 M9 r9, S10 M10 r10, S11 M11 r11, S12 M12 r12, S13 M13 r13, S14 M14 r14
);
impl_group_runner_tuple!(
S0 M0 r0, S1 M1 r1, S2 M2 r2, S3 M3 r3, S4 M4 r4, S5 M5 r5, S6 M6 r6, S7 M7 r7, S8 M8 r8,
S9 M9 r9, S10 M10 r10, S11 M11 r11, S12 M12 r12, S13 M13 r13, S14 M14 r14, S15 M15 r15
);
pub fn run_if<W, Marker>(
condition: impl Fn(&W) -> bool + Send + 'static,
systems: impl IntoGroupRunner<W, Marker>,
) -> impl FnMut(&mut W) + Send + 'static {
let mut runner = systems.into_group_runner();
move |world: &mut W| {
if condition(world) {
runner(world);
}
}
}
pub fn while_in<S, W, Marker>(
state: S,
systems: impl IntoGroupRunner<W, Marker>,
) -> impl FnMut(&mut W) + Send + 'static
where
S: Copy + PartialEq + Send + Sync + 'static,
W: ResourceHost,
{
run_if(move |world: &W| in_state::<S, W>(world, state), systems)
}
pub fn while_in_any<S, W, Marker>(
states: impl IntoIterator<Item = S>,
systems: impl IntoGroupRunner<W, Marker>,
) -> impl FnMut(&mut W) + Send + 'static
where
S: Copy + PartialEq + Send + Sync + 'static,
W: ResourceHost,
{
let states: Vec<S> = states.into_iter().collect();
run_if(
move |world: &W| states.contains(¤t_state::<S, W>(world)),
systems,
)
}
pub fn on_enter<S, W, Marker>(
state: S,
systems: impl IntoGroupRunner<W, Marker>,
) -> impl FnMut(&mut W) + Send + 'static
where
S: Copy + PartialEq + Send + Sync + 'static,
W: EventHost + 'static,
{
let mut cursor = 0u64;
let mut runner = systems.into_group_runner();
move |host: &mut W| {
let entered = host
.event_bus_mut()
.consume::<StateTransition<S>>(&mut cursor)
.iter()
.any(|transition| transition.after == state);
if entered {
runner(host);
}
}
}
pub fn on_exit<S, W, Marker>(
state: S,
systems: impl IntoGroupRunner<W, Marker>,
) -> impl FnMut(&mut W) + Send + 'static
where
S: Copy + PartialEq + Send + Sync + 'static,
W: EventHost + 'static,
{
let mut cursor = 0u64;
let mut runner = systems.into_group_runner();
move |host: &mut W| {
let exited = host
.event_bus_mut()
.consume::<StateTransition<S>>(&mut cursor)
.iter()
.any(|transition| transition.before == Some(state));
if exited {
runner(host);
}
}
}
pub trait StateScheduleExt<W> {
fn add_state_transitions<S>(&mut self, name: &'static str) -> &mut Self
where
S: Copy + PartialEq + Send + Sync + 'static;
}
impl<W: ResourceHost + EventHost + 'static> StateScheduleExt<W> for Schedule<W> {
fn add_state_transitions<S>(&mut self, name: &'static str) -> &mut Self
where
S: Copy + PartialEq + Send + Sync + 'static,
{
self.push(name, apply_state_transition::<S, W>)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Schedule;
use crate::dynamic::DynWorld;
use crate::system_param::{EventReader, ResMut, ScheduleExt};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Screen {
Title,
Playing,
Paused,
}
struct Ticks(u32);
struct Entered(u32);
struct Exited(u32);
fn state_schedule() -> Schedule<DynWorld> {
let mut schedule = Schedule::new();
schedule.add_state_transitions::<Screen>("screen_transitions");
schedule
}
#[test]
fn insert_and_read_state() {
let mut world = DynWorld::new();
insert_state(&mut world, Screen::Title);
assert_eq!(current_state::<Screen, _>(&world), Screen::Title);
assert!(in_state(&world, Screen::Title));
assert!(!in_state(&world, Screen::Playing));
}
#[test]
fn transition_applies_on_next_run() {
let mut world = DynWorld::new();
insert_state(&mut world, Screen::Title);
let mut schedule = state_schedule();
schedule.run(&mut world);
assert_eq!(current_state::<Screen, _>(&world), Screen::Title);
next_state(&mut world, Screen::Playing);
assert_eq!(
current_state::<Screen, _>(&world),
Screen::Title,
"the request is pending until the next transition step"
);
schedule.run(&mut world);
assert_eq!(current_state::<Screen, _>(&world), Screen::Playing);
}
#[test]
fn request_for_current_state_is_dropped() {
let mut world = DynWorld::new();
insert_state(&mut world, Screen::Title);
world.insert_resource(Entered(0));
let mut schedule = state_schedule();
schedule.push("on_enter_title", on_enter(Screen::Title, count_enter));
schedule.run(&mut world);
assert_eq!(world.resource::<Entered>().unwrap().0, 1);
next_state(&mut world, Screen::Title);
schedule.run(&mut world);
assert_eq!(
world.resource::<Entered>().unwrap().0,
1,
"a no-op request emits no transition, so enter does not fire again"
);
}
fn tick(mut ticks: ResMut<Ticks>) {
ticks.0 += 1;
}
fn count_enter(mut entered: ResMut<Entered>) {
entered.0 += 1;
}
fn count_exit(mut exited: ResMut<Exited>) {
exited.0 += 1;
}
#[test]
fn while_in_gates_a_single_system() {
let mut world = DynWorld::new();
insert_state(&mut world, Screen::Title);
world.insert_resource(Ticks(0));
let mut schedule = state_schedule();
schedule.push("tick", while_in(Screen::Playing, tick));
schedule.run(&mut world);
assert_eq!(world.resource::<Ticks>().unwrap().0, 0);
next_state(&mut world, Screen::Playing);
schedule.run(&mut world);
assert_eq!(world.resource::<Ticks>().unwrap().0, 1);
}
#[test]
fn while_in_gates_a_tuple_as_one_entry() {
let mut world = DynWorld::new();
insert_state(&mut world, Screen::Playing);
world.insert_resource(Ticks(0));
world.insert_resource(Entered(0));
let mut schedule = state_schedule();
schedule.push(
"group",
while_in(Screen::Playing, (tick, count_enter, tick)),
);
schedule.run(&mut world);
assert_eq!(world.resource::<Ticks>().unwrap().0, 2);
assert_eq!(world.resource::<Entered>().unwrap().0, 1);
}
#[test]
fn while_in_any_covers_several_states() {
let mut world = DynWorld::new();
insert_state(&mut world, Screen::Paused);
world.insert_resource(Ticks(0));
let mut schedule = state_schedule();
schedule.push("hud", while_in_any([Screen::Playing, Screen::Paused], tick));
schedule.run(&mut world);
assert_eq!(world.resource::<Ticks>().unwrap().0, 1);
next_state(&mut world, Screen::Title);
schedule.run(&mut world);
assert_eq!(
world.resource::<Ticks>().unwrap().0,
1,
"Title is not in the gate set"
);
}
#[test]
fn run_if_gates_on_a_plain_condition() {
let mut world = DynWorld::new();
world.insert_resource(Ticks(0));
world.insert_resource(Entered(0));
let mut schedule = Schedule::new();
schedule.push(
"tick",
run_if(
|world: &DynWorld| world.resource::<Entered>().unwrap().0 == 0,
tick,
),
);
schedule.run(&mut world);
assert_eq!(world.resource::<Ticks>().unwrap().0, 1);
world.resource_mut::<Entered>().unwrap().0 = 1;
schedule.run(&mut world);
assert_eq!(world.resource::<Ticks>().unwrap().0, 1);
}
#[test]
fn on_enter_and_on_exit_fire_once_per_transition() {
let mut world = DynWorld::new();
insert_state(&mut world, Screen::Title);
world.insert_resource(Entered(0));
world.insert_resource(Exited(0));
let mut schedule = state_schedule();
schedule.push("enter_playing", on_enter(Screen::Playing, count_enter));
schedule.push("exit_title", on_exit(Screen::Title, count_exit));
schedule.run(&mut world);
assert_eq!(world.resource::<Entered>().unwrap().0, 0);
assert_eq!(world.resource::<Exited>().unwrap().0, 0);
next_state(&mut world, Screen::Playing);
schedule.run(&mut world);
assert_eq!(world.resource::<Entered>().unwrap().0, 1);
assert_eq!(world.resource::<Exited>().unwrap().0, 1);
schedule.run(&mut world);
assert_eq!(world.resource::<Entered>().unwrap().0, 1);
assert_eq!(world.resource::<Exited>().unwrap().0, 1);
}
#[test]
fn initial_entry_emits_a_transition_event() {
let mut world = DynWorld::new();
insert_state(&mut world, Screen::Title);
world.insert_resource(Seen(Vec::new()));
let mut schedule = state_schedule();
schedule.add_system("record", record_transitions);
schedule.run(&mut world);
assert_eq!(
world.resource::<Seen>().unwrap().0,
vec![(None, Screen::Title)]
);
}
struct Seen(Vec<(Option<Screen>, Screen)>);
fn record_transitions(reader: EventReader<StateTransition<Screen>>, mut seen: ResMut<Seen>) {
for transition in &reader {
seen.0.push((transition.before, transition.after));
}
}
}