bevy_roll_safe/
lib.rs

1use std::marker::PhantomData;
2
3use bevy::{ecs::schedule::ScheduleLabel, prelude::*, state::state::FreelyMutableState};
4
5mod frame_count;
6
7// re-exports
8pub use frame_count::{increase_frame_count, RollFrameCount};
9
10pub mod prelude {
11    pub use super::RollApp;
12}
13
14pub trait RollApp {
15    /// Init state transitions in the given schedule
16    fn init_roll_state<S: States + FromWorld + FreelyMutableState>(
17        &mut self,
18        schedule: impl ScheduleLabel,
19    ) -> &mut Self;
20
21    #[cfg(feature = "bevy_ggrs")]
22    /// Register this state to be rolled back by bevy_ggrs
23    fn init_ggrs_state<S: States + FromWorld + Clone + FreelyMutableState>(&mut self) -> &mut Self;
24
25    #[cfg(feature = "bevy_ggrs")]
26    /// Register this state to be rolled back by bevy_ggrs in the specified schedule
27    fn init_ggrs_state_in_schedule<S: States + FromWorld + Clone + FreelyMutableState>(
28        &mut self,
29        schedule: impl ScheduleLabel,
30    ) -> &mut Self;
31}
32
33impl RollApp for App {
34    fn init_roll_state<S: States + FromWorld + FreelyMutableState>(
35        &mut self,
36        schedule: impl ScheduleLabel,
37    ) -> &mut Self {
38        if !self.world().contains_resource::<State<S>>() {
39            self.init_resource::<State<S>>()
40                .init_resource::<NextState<S>>()
41                .init_resource::<InitialStateEntered<S>>()
42                // events are not rollback safe, but `apply_state_transition` will cause errors without it
43                .add_event::<StateTransitionEvent<S>>()
44                .add_systems(
45                    schedule,
46                    (
47                        run_enter_schedule::<S>
48                            .run_if(resource_equals(InitialStateEntered::<S>(false, default()))),
49                        mark_state_initialized::<S>
50                            .run_if(resource_equals(InitialStateEntered::<S>(false, default()))),
51                        apply_state_transition::<S>,
52                    )
53                        .chain(),
54                );
55        } else {
56            let name = std::any::type_name::<S>();
57            warn!("State {} is already initialized.", name);
58        }
59
60        self
61    }
62
63    #[cfg(feature = "bevy_ggrs")]
64    fn init_ggrs_state<S: States + FromWorld + Clone + FreelyMutableState>(&mut self) -> &mut Self {
65        use bevy_ggrs::GgrsSchedule;
66        self.init_ggrs_state_in_schedule::<S>(GgrsSchedule)
67    }
68
69    #[cfg(feature = "bevy_ggrs")]
70    fn init_ggrs_state_in_schedule<S: States + FromWorld + Clone + FreelyMutableState>(
71        &mut self,
72        schedule: impl ScheduleLabel,
73    ) -> &mut Self {
74        use crate::ggrs_support::{NextStateStrategy, StateStrategy};
75        use bevy_ggrs::{CloneStrategy, ResourceSnapshotPlugin};
76
77        self.init_roll_state::<S>(schedule).add_plugins((
78            ResourceSnapshotPlugin::<StateStrategy<S>>::default(),
79            ResourceSnapshotPlugin::<NextStateStrategy<S>>::default(),
80            ResourceSnapshotPlugin::<CloneStrategy<InitialStateEntered<S>>>::default(),
81        ))
82    }
83}
84
85#[cfg(feature = "bevy_ggrs")]
86mod ggrs_support {
87    use bevy::{prelude::*, state::state::FreelyMutableState};
88    use bevy_ggrs::Strategy;
89    use std::marker::PhantomData;
90
91    pub(crate) struct StateStrategy<S: States>(PhantomData<S>);
92
93    // todo: make State<S> implement clone instead
94    impl<S: States> Strategy for StateStrategy<S> {
95        type Target = State<S>;
96        type Stored = S;
97
98        fn store(target: &Self::Target) -> Self::Stored {
99            target.get().to_owned()
100        }
101
102        fn load(stored: &Self::Stored) -> Self::Target {
103            State::new(stored.to_owned())
104        }
105    }
106
107    pub(crate) struct NextStateStrategy<S: States>(PhantomData<S>);
108
109    // todo: make NextState<S> implement clone instead
110    impl<S: States + FreelyMutableState> Strategy for NextStateStrategy<S> {
111        type Target = NextState<S>;
112        type Stored = Option<S>;
113
114        fn store(target: &Self::Target) -> Self::Stored {
115            match target {
116                NextState::Unchanged => None,
117                NextState::Pending(s) => Some(s.to_owned()),
118            }
119        }
120
121        fn load(stored: &Self::Stored) -> Self::Target {
122            match stored {
123                None => NextState::Unchanged,
124                Some(s) => NextState::Pending(s.to_owned()),
125            }
126        }
127    }
128}
129
130#[derive(Resource, Debug, Reflect, Eq, PartialEq, Clone)]
131#[reflect(Resource)]
132pub struct InitialStateEntered<S: States>(bool, PhantomData<S>);
133
134impl<S: States> Default for InitialStateEntered<S> {
135    fn default() -> Self {
136        Self(false, default())
137    }
138}
139
140fn mark_state_initialized<S: States + FromWorld>(
141    mut state_initialized: ResMut<InitialStateEntered<S>>,
142) {
143    state_initialized.0 = true;
144}
145
146/// Run the enter schedule (if it exists) for the current state.
147pub fn run_enter_schedule<S: States>(world: &mut World) {
148    let Some(state) = world.get_resource::<State<S>>() else {
149        return;
150    };
151    world.try_run_schedule(OnEnter(state.get().clone())).ok();
152}
153
154/// If a new state is queued in [`NextState<S>`], this system:
155/// - Takes the new state value from [`NextState<S>`] and updates [`State<S>`].
156/// - Sends a relevant [`StateTransitionEvent`]
157/// - Runs the [`OnExit(exited_state)`] schedule, if it exists.
158/// - Runs the [`OnTransition { from: exited_state, to: entered_state }`](OnTransition), if it exists.
159/// - Runs the [`OnEnter(entered_state)`] schedule, if it exists.
160pub fn apply_state_transition<S: States + FreelyMutableState>(world: &mut World) {
161    // We want to take the `NextState` resource,
162    // but only mark it as changed if it wasn't empty.
163    let Some(mut next_state_resource) = world.get_resource_mut::<NextState<S>>() else {
164        return;
165    };
166    if let NextState::Pending(entered) = next_state_resource.bypass_change_detection() {
167        let entered = entered.clone();
168        *next_state_resource = NextState::Unchanged;
169        match world.get_resource_mut::<State<S>>() {
170            Some(mut state_resource) => {
171                if *state_resource != entered {
172                    let exited = state_resource.get().clone();
173                    *state_resource = State::new(entered.clone());
174                    world.send_event(StateTransitionEvent {
175                        exited: Some(exited.clone()),
176                        entered: Some(entered.clone()),
177                    });
178                    // Try to run the schedules if they exist.
179                    world.try_run_schedule(OnExit(exited.clone())).ok();
180                    world
181                        .try_run_schedule(OnTransition {
182                            exited,
183                            entered: entered.clone(),
184                        })
185                        .ok();
186                    world.try_run_schedule(OnEnter(entered)).ok();
187                }
188            }
189            None => {
190                world.insert_resource(State::new(entered.clone()));
191                world.try_run_schedule(OnEnter(entered)).ok();
192            }
193        };
194    }
195}