Skip to main content

bevy_roll_safe/
lib.rs

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