1use std::collections::HashMap;
26use std::hash::Hash;
27
28use bevy_ecs::prelude::*;
29use bevy_ecs::schedule::{IntoScheduleConfigs, Schedule};
30use bevy_ecs::system::ScheduleSystem;
31
32pub trait States: Clone + Eq + Hash + Send + Sync + 'static {}
37
38impl<T> States for T where T: Clone + Eq + Hash + Send + Sync + 'static {}
39
40#[derive(Resource)]
42pub struct State<S: States>(S);
43
44impl<S: States> State<S> {
45 pub fn new(value: S) -> Self {
46 Self(value)
47 }
48
49 pub fn get(&self) -> &S {
50 &self.0
51 }
52}
53
54#[derive(Resource)]
57pub struct NextState<S: States>(Option<S>);
58
59impl<S: States> NextState<S> {
60 pub fn new() -> Self {
61 Self(None)
62 }
63
64 pub fn set(&mut self, next: S) {
66 self.0 = Some(next);
67 }
68
69 pub fn pending(&self) -> Option<&S> {
71 self.0.as_ref()
72 }
73
74 fn take(&mut self) -> Option<S> {
75 self.0.take()
76 }
77}
78
79impl<S: States> Default for NextState<S> {
80 fn default() -> Self {
81 Self(None)
82 }
83}
84
85#[derive(Resource)]
87pub(crate) struct StateSchedules<S: States> {
88 on_enter: HashMap<S, Schedule>,
89 on_exit: HashMap<S, Schedule>,
90 on_transition: HashMap<(S, S), Schedule>,
91}
92
93impl<S: States> Default for StateSchedules<S> {
94 fn default() -> Self {
95 Self {
96 on_enter: HashMap::new(),
97 on_exit: HashMap::new(),
98 on_transition: HashMap::new(),
99 }
100 }
101}
102
103pub const MAX_CHAIN: u32 = 8;
105
106pub(crate) fn apply<S: States>(world: &mut World) {
109 for _ in 0..MAX_CHAIN {
110 let Some(next) = world.resource_mut::<NextState<S>>().take() else {
111 break;
112 };
113 let current = world.resource::<State<S>>().get().clone();
114 if next == current {
115 continue;
116 }
117 world.resource_scope(|world, mut schedules: Mut<StateSchedules<S>>| {
118 if let Some(schedule) = schedules.on_exit.get_mut(¤t) {
119 schedule.run(world);
120 }
121 world.resource_mut::<State<S>>().0 = next.clone();
122 if let Some(schedule) = schedules.on_transition.get_mut(&(current, next.clone())) {
123 schedule.run(world);
124 }
125 if let Some(schedule) = schedules.on_enter.get_mut(&next) {
126 schedule.run(world);
127 }
128 });
129 }
130}
131
132pub fn in_state<S: States>(state: S) -> impl FnMut(Res<State<S>>) -> bool + Clone + Send + Sync + 'static {
138 move |current: Res<State<S>>| current.get() == &state
139}
140
141pub(crate) fn init_schedules<S: States>(world: &mut World) {
142 world.insert_resource(StateSchedules::<S>::default());
143}
144
145pub(crate) fn add_enter<S, M>(
146 world: &mut World,
147 state: S,
148 systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
149) where
150 S: States,
151{
152 world
153 .resource_mut::<StateSchedules<S>>()
154 .on_enter
155 .entry(state)
156 .or_default()
157 .add_systems(systems);
158}
159
160pub(crate) fn add_exit<S, M>(
161 world: &mut World,
162 state: S,
163 systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
164) where
165 S: States,
166{
167 world
168 .resource_mut::<StateSchedules<S>>()
169 .on_exit
170 .entry(state)
171 .or_default()
172 .add_systems(systems);
173}
174
175pub(crate) fn add_transition<S, M>(
176 world: &mut World,
177 exited: S,
178 entered: S,
179 systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
180) where
181 S: States,
182{
183 world
184 .resource_mut::<StateSchedules<S>>()
185 .on_transition
186 .entry((exited, entered))
187 .or_default()
188 .add_systems(systems);
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
196 enum Mode {
197 Menu,
198 Play,
199 Paused,
200 }
201
202 #[test]
203 fn transition_runs_exit_then_enter_in_order() {
204 use std::sync::{Arc, Mutex};
205
206 let log: Arc<Mutex<Vec<&'static str>>> = Arc::new(Mutex::new(Vec::new()));
207 let mut sim = crate::Sim::with_default_step();
208 sim.add_state(Mode::Menu);
209
210 let (a, b, c) = (log.clone(), log.clone(), log.clone());
211 sim.on_exit(Mode::Menu, move || a.lock().unwrap().push("exit:menu"));
212 sim.on_enter(Mode::Play, move || b.lock().unwrap().push("enter:play"));
213 sim.on_transition(Mode::Menu, Mode::Play, move || {
214 c.lock().unwrap().push("transition:menu->play")
215 });
216
217 sim.set_state(Mode::Play);
218 sim.tick();
219 assert_eq!(
220 *log.lock().unwrap(),
221 ["exit:menu", "transition:menu->play", "enter:play"]
222 );
223 assert_eq!(*sim.world.resource::<State<Mode>>().get(), Mode::Play);
224 }
225
226 #[test]
227 fn exit_sees_old_state_and_enter_sees_new_state() {
228 use std::sync::{Arc, Mutex};
229
230 let seen: Arc<Mutex<Vec<Mode>>> = Arc::new(Mutex::new(Vec::new()));
231 let mut sim = crate::Sim::with_default_step();
232 sim.add_state(Mode::Menu);
233
234 let e = seen.clone();
235 sim.on_exit(Mode::Menu, move |s: Res<State<Mode>>| e.lock().unwrap().push(*s.get()));
236 let n = seen.clone();
237 sim.on_enter(Mode::Play, move |s: Res<State<Mode>>| n.lock().unwrap().push(*s.get()));
238
239 sim.set_state(Mode::Play);
240 sim.tick();
241 assert_eq!(*seen.lock().unwrap(), [Mode::Menu, Mode::Play]);
242 }
243
244 #[test]
245 fn a_transition_requested_on_enter_chains_on_the_same_step() {
246 let mut sim = crate::Sim::with_default_step();
247 sim.add_state(Mode::Menu);
248 sim.on_enter(Mode::Play, |mut next: ResMut<NextState<Mode>>| {
249 next.set(Mode::Paused)
250 });
251 sim.set_state(Mode::Play);
252 sim.tick();
253 assert_eq!(*sim.world.resource::<State<Mode>>().get(), Mode::Paused);
254 }
255
256 #[test]
257 fn self_triggering_transition_is_bounded() {
258 let mut sim = crate::Sim::with_default_step();
259 sim.add_state(Mode::Menu);
260 sim.on_enter(Mode::Play, |mut next: ResMut<NextState<Mode>>| {
261 next.set(Mode::Play)
262 });
263 sim.set_state(Mode::Play);
264 sim.tick();
265 assert_eq!(*sim.world.resource::<State<Mode>>().get(), Mode::Play);
266 assert!(sim.world.resource::<NextState<Mode>>().pending().is_none());
267 }
268
269 #[test]
270 fn set_to_current_state_runs_no_schedules() {
271 use std::sync::{Arc, Mutex};
272 let fired = Arc::new(Mutex::new(false));
273 let mut sim = crate::Sim::with_default_step();
274 sim.add_state(Mode::Menu);
275 let f = fired.clone();
276 sim.on_enter(Mode::Menu, move || *f.lock().unwrap() = true);
277 sim.set_state(Mode::Menu);
278 sim.tick();
279 assert!(!*fired.lock().unwrap());
280 }
281
282 #[test]
283 fn in_state_condition_gates_systems() {
284 use std::sync::atomic::{AtomicU32, Ordering};
285 static HITS: AtomicU32 = AtomicU32::new(0);
286
287 let mut sim = crate::Sim::with_default_step();
288 sim.add_state(Mode::Menu);
289 sim.add_chained_systems(
290 (|| {
291 HITS.fetch_add(1, Ordering::SeqCst);
292 })
293 .run_if(in_state(Mode::Menu)),
294 );
295 sim.tick();
296 assert_eq!(HITS.load(Ordering::SeqCst), 1);
297 sim.set_state(Mode::Play);
298 sim.tick();
299 assert_eq!(HITS.load(Ordering::SeqCst), 1, "gated out after switching");
300 }
301}