use bevy::{
ecs::schedule::{
ExecutorKind, InternedScheduleLabel, LogLevel, ScheduleBuildSettings, ScheduleLabel,
},
prelude::*,
};
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)]
pub struct RollbackStateTransition;
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)]
pub struct RollbackPreUpdate;
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)]
pub struct RollbackUpdate;
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)]
pub struct RollbackPostUpdate;
pub struct RollbackSchedulePlugin {
schedule: InternedScheduleLabel,
}
impl RollbackSchedulePlugin {
pub fn new(schedule: impl ScheduleLabel + 'static) -> Self {
Self {
schedule: schedule.intern(),
}
}
#[cfg(feature = "bevy_ggrs")]
pub fn new_ggrs() -> Self {
Self::new(bevy_ggrs::GgrsSchedule)
}
}
impl Plugin for RollbackSchedulePlugin {
fn build(&self, app: &mut App) {
let mut rollback_schedule = Schedule::new(self.schedule);
rollback_schedule.set_executor_kind(ExecutorKind::SingleThreaded);
for label in RollbackScheduleOrder::default().labels {
app.edit_schedule(label, |schedule| {
schedule.set_build_settings(ScheduleBuildSettings {
ambiguity_detection: LogLevel::Error,
..default()
});
});
}
app.insert_resource(RollbackScheduleOrder::default())
.add_systems(self.schedule, run_schedules);
}
}
#[derive(Resource, Debug)]
struct RollbackScheduleOrder {
pub labels: Vec<InternedScheduleLabel>,
}
impl Default for RollbackScheduleOrder {
fn default() -> Self {
Self {
labels: vec![
RollbackPreUpdate.intern(),
RollbackStateTransition.intern(),
RollbackUpdate.intern(),
RollbackPostUpdate.intern(),
],
}
}
}
fn run_schedules(world: &mut World) {
world.resource_scope(|world, order: Mut<RollbackScheduleOrder>| {
for label in &order.labels {
trace!("Running rollback schedule: {:?}", label);
let _ = world.try_run_schedule(*label);
}
});
}
#[cfg(test)]
mod tests {
use crate::{InitialStateEntered, RollApp};
use super::*;
#[derive(Resource, Debug, Default)]
struct IntResource(i32);
fn increase_int_resource(mut int_resource: ResMut<IntResource>) {
int_resource.0 += 1;
}
#[test]
fn rollback_schedule_in_update() {
let mut app = App::new();
app.add_plugins(RollbackSchedulePlugin::new(Update));
app.init_resource::<IntResource>();
app.add_systems(RollbackUpdate, increase_int_resource);
app.update();
assert_eq!(
app.world().resource::<IntResource>().0,
1,
"IntResource should be incremented by 1"
);
app.update();
assert_eq!(
app.world().resource::<IntResource>().0,
2,
"IntResource should be incremented by 1 two times"
);
}
#[derive(States, Hash, Default, Debug, Eq, PartialEq, Clone)]
enum GameplayState {
#[default]
InRound,
GameOver,
}
#[test]
fn add_states_to_rollback_schedule() {
let mut app = App::new();
app.add_plugins(RollbackSchedulePlugin::new(Update));
app.init_resource::<IntResource>();
app.init_roll_state::<GameplayState>();
app.add_systems(OnEnter(GameplayState::InRound), increase_int_resource);
assert!(app.world().contains_resource::<State<GameplayState>>());
assert!(app.world().contains_resource::<NextState<GameplayState>>());
assert_eq!(app.world().resource::<IntResource>().0, 0);
assert!(
!app.world()
.resource::<InitialStateEntered<GameplayState>>()
.0
);
app.update();
assert!(
app.world()
.resource::<InitialStateEntered<GameplayState>>()
.0
);
assert_eq!(app.world().resource::<IntResource>().0, 1);
}
#[test]
#[should_panic(expected = "RollbackStateTransition")]
fn init_ggrs_states_without_rollback_state_transition_schedule_panics() {
App::new().init_ggrs_state::<GameplayState>();
}
fn set_game_over_state(mut next_state: ResMut<NextState<GameplayState>>) {
next_state.set(GameplayState::GameOver);
}
#[test]
#[cfg(feature = "bevy_ggrs")]
fn can_roll_back_states() {
use bevy_ggrs::{AdvanceWorld, GgrsSchedule, LoadWorld, SaveWorld, SnapshotPlugin};
let mut app = App::new();
app.add_plugins(SnapshotPlugin)
.add_plugins(RollbackSchedulePlugin::new_ggrs())
.add_systems(AdvanceWorld, |world: &mut World| {
dbg!("Advancing world in GgrsSchedule");
world.try_run_schedule(GgrsSchedule).unwrap();
})
.add_systems(RollbackUpdate, || {
dbg!("RollbackUpdate");
})
.init_resource::<IntResource>()
.init_ggrs_state::<GameplayState>()
.add_systems(
RollbackUpdate,
set_game_over_state.run_if(in_state(GameplayState::InRound)),
);
assert_eq!(
*app.world().resource::<State<GameplayState>>(),
GameplayState::InRound
);
assert!(matches!(
app.world().resource::<NextState<GameplayState>>(),
NextState::Unchanged,
));
app.world_mut().run_schedule(SaveWorld);
app.world_mut().run_schedule(AdvanceWorld);
assert_eq!(
*app.world().resource::<State<GameplayState>>(),
GameplayState::InRound,
"State should not change until the next frame"
);
assert!(matches!(
app.world().resource::<NextState<GameplayState>>(),
NextState::Pending(GameplayState::GameOver)
));
app.world_mut().run_schedule(AdvanceWorld);
assert_eq!(
*app.world().resource::<State<GameplayState>>(),
GameplayState::GameOver,
);
assert!(matches!(
app.world().resource::<NextState<GameplayState>>(),
NextState::Unchanged
));
app.world_mut().run_schedule(LoadWorld);
assert_eq!(
*app.world().resource::<State<GameplayState>>(),
GameplayState::InRound,
);
assert!(matches!(
app.world().resource::<NextState<GameplayState>>(),
NextState::Unchanged,
));
}
}