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#[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 fn init_roll_state_in_schedule<S: States + FromWorld + FreelyMutableState>(
36 &mut self,
37 schedule: impl ScheduleLabel,
38 ) -> &mut Self;
39
40 fn init_roll_state<S: States + FromWorld + FreelyMutableState>(&mut self) -> &mut Self;
42
43 #[cfg(feature = "bevy_ggrs")]
44 fn init_ggrs_state<S: States + FromWorld + Clone + FreelyMutableState>(&mut self) -> &mut Self;
46
47 #[cfg(feature = "bevy_ggrs")]
48 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_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 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 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 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
181pub 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
189pub fn apply_state_transition<S: States + FreelyMutableState>(world: &mut World) {
196 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.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}