#![warn(missing_docs)]
use backroll::{
command::{Command, Commands},
Config, Event, GameInput, PlayerHandle,
};
use bevy_app::{App, CoreStage, Events, Plugin};
use bevy_ecs::{
prelude::*,
schedule::{IntoSystemDescriptor, ShouldRun, Stage, SystemSet, SystemStage},
system::{Commands as BevyCommands, System},
world::World,
};
use bevy_log::{debug, error};
use parking_lot::Mutex;
use std::marker::PhantomData;
use std::sync::Arc;
mod id;
mod save_state;
#[cfg(feature = "steam")]
mod steam;
pub use backroll;
pub use id::*;
use save_state::*;
pub type P2PSession<Input> = backroll::P2PSession<BevyBackrollConfig<Input>>;
#[derive(Debug, Clone, Eq, Hash, StageLabel, PartialEq)]
pub struct BackrollUpdate;
struct FrameStaller {
frames_ahead: u8,
frames_until_stall: u8,
}
impl FrameStaller {
pub fn new() -> Self {
Self {
frames_ahead: 0,
frames_until_stall: 0,
}
}
pub fn reset(&mut self, frames_ahead: u8) {
self.frames_ahead = frames_ahead;
self.frames_until_stall = self.stall_cadence();
}
pub fn should_stall(&mut self) -> bool {
if self.frames_ahead == 0 {
return false;
}
if self.frames_until_stall == 0 {
self.frames_ahead -= 1;
self.frames_until_stall = self.stall_cadence();
true
} else {
self.frames_until_stall -= 1;
false
}
}
fn stall_cadence(&self) -> u8 {
if self.frames_ahead > 9 {
1
} else {
11 - self.frames_ahead
}
}
}
struct BackrollStagesRef {
save: SystemStage,
simulate: SystemStage,
before_load: SystemStage,
load: SystemStage,
run_criteria: Option<Box<dyn System<In = (), Out = ShouldRun>>>,
}
#[derive(Clone)]
struct BackrollStages(Arc<Mutex<BackrollStagesRef>>);
pub struct BevyBackrollConfig<Input> {
_marker: PhantomData<Input>,
}
impl<Input: PartialEq + bytemuck::Pod + bytemuck::Zeroable + Send + Sync> Config
for BevyBackrollConfig<Input>
{
type Input = Input;
type State = SaveState;
}
pub struct BackrollStage<Input>
where
Input: PartialEq + bytemuck::Pod + bytemuck::Zeroable + Send + Sync,
{
staller: FrameStaller,
input_sample_fn: Box<dyn System<In = PlayerHandle, Out = Input> + Send + Sync + 'static>,
}
impl<Input> BackrollStage<Input>
where
Input: PartialEq + bytemuck::Pod + bytemuck::Zeroable + Send + Sync,
{
fn run_commands(&mut self, commands: Commands<BevyBackrollConfig<Input>>, world: &mut World) {
let stage = world.get_resource::<BackrollStages>().unwrap().clone();
for command in commands {
match command {
Command::Save(save_state) => {
world.insert_resource(SaveStateBuilder::new());
stage.0.lock().save.run(world);
save_state.save_without_hash(
world.remove_resource::<SaveStateBuilder>().unwrap().build(),
);
}
Command::Load(load_state) => {
world.insert_resource(load_state.load());
{
let mut stage = stage.0.lock();
stage.before_load.run(world);
stage.load.run(world);
}
world.remove_resource::<SaveState>();
}
Command::AdvanceFrame(inputs) => {
*world.get_resource_mut::<GameInput<Input>>().unwrap() = inputs;
stage.0.lock().simulate.run(world);
}
Command::Event(evt) => {
debug!("Received Backroll Event: {:?}", evt);
if let Event::TimeSync { frames_ahead } = &evt {
self.staller.reset(*frames_ahead);
}
let mut events = world.get_resource_mut::<Events<Event>>().unwrap();
events.send(evt.clone());
}
}
}
}
}
impl<Input> Stage for BackrollStage<Input>
where
Input: PartialEq + bytemuck::Pod + bytemuck::Zeroable + Send + Sync,
{
fn run(&mut self, world: &mut World) {
loop {
let should_run = {
let stages = world.get_resource::<BackrollStages>().unwrap().clone();
let run_criteria = &mut stages.0.lock().run_criteria;
if let Some(ref mut run_criteria) = run_criteria {
run_criteria.run((), world)
} else {
ShouldRun::Yes
}
};
if let ShouldRun::No = should_run {
return;
}
let session = if let Some(session) = world.get_resource_mut::<P2PSession<Input>>() {
session.clone()
} else {
return;
};
self.run_commands(session.poll(), world);
if self.staller.should_stall() {
continue;
}
for player_handle in session.local_players() {
let input = self.input_sample_fn.run(player_handle, world);
if let Err(err) = session.add_local_input(player_handle, input) {
error!(
"Error while adding local input for {:?}: {:?}",
player_handle, err
);
return;
}
}
self.run_commands(session.advance_frame(), world);
if let ShouldRun::Yes = should_run {
return;
}
}
}
}
#[derive(Default)]
pub struct BackrollPlugin;
impl Plugin for BackrollPlugin {
fn build(&self, app: &mut App) {
let mut save = SystemStage::parallel();
save.add_system(save_network_ids);
let mut before_load = SystemStage::parallel();
before_load.add_system(sync_network_ids);
app.add_event::<backroll::Event>()
.insert_resource(NetworkIdProvider::new())
.insert_resource(BackrollStages(Arc::new(Mutex::new(BackrollStagesRef {
save,
simulate: SystemStage::parallel(),
before_load,
load: SystemStage::parallel(),
run_criteria: None,
}))))
.register_rollback_resource::<NetworkIdProvider>();
#[cfg(feature = "steam")]
app.add_plugin(steam::BackrollSteamPlugin);
}
}
pub trait BackrollApp {
fn register_rollback_input<Input, S>(&mut self, system: S) -> &mut Self
where
Input: PartialEq + bytemuck::Pod + bytemuck::Zeroable + Send + Sync,
S: System<In = PlayerHandle, Out = Input> + Send + Sync + 'static;
fn register_rollback_component<T: Component + Clone>(&mut self) -> &mut Self;
fn register_rollback_resource<T: Clone + Send + Sync + 'static>(&mut self) -> &mut Self;
fn with_rollback_run_criteria<Input, S>(&mut self, system: S) -> &mut Self
where
S: System<In = (), Out = ShouldRun>;
fn add_rollback_system<S, U>(&mut self, system: S) -> &mut Self
where
S: IntoSystemDescriptor<U>;
fn add_rollback_system_set(&mut self, system: impl Into<SystemSet>) -> &mut Self;
}
impl BackrollApp for App {
fn register_rollback_input<Input, S>(&mut self, system: S) -> &mut Self
where
Input: PartialEq + bytemuck::Pod + bytemuck::Zeroable + Send + Sync,
S: System<In = PlayerHandle, Out = Input> + Send + Sync + 'static,
{
self.insert_resource(GameInput::<Input>::default());
self.add_stage_before(
CoreStage::Update,
BackrollUpdate,
BackrollStage::<Input> {
staller: FrameStaller::new(),
input_sample_fn: Box::new(system),
},
);
self
}
fn register_rollback_component<T: Component + Clone>(&mut self) -> &mut Self {
{
let mut stages = self
.world
.get_resource::<BackrollStages>()
.expect("No BackrollStages found! Did you install the plugin?")
.0
.lock();
stages.load.add_system(load_components::<T>);
stages.save.add_system(save_components::<T>);
}
self
}
fn register_rollback_resource<T: Clone + Send + Sync + 'static>(&mut self) -> &mut Self {
{
let mut stages = self
.world
.get_resource::<BackrollStages>()
.expect("No BackrollStages found! Did you install the plugin?")
.0
.lock();
stages.load.add_system(load_resource::<T>);
stages.save.add_system(save_resource::<T>);
}
self
}
fn with_rollback_run_criteria<Input, S>(&mut self, mut run_criteria: S) -> &mut Self
where
S: System<In = (), Out = ShouldRun>,
{
run_criteria.initialize(&mut self.world);
self.world
.get_resource::<BackrollStages>()
.expect("No BackrollStages found! Did you install the plugin?")
.0
.lock()
.run_criteria = Some(Box::new(run_criteria));
self
}
fn add_rollback_system<S, U>(&mut self, system: S) -> &mut Self
where
S: IntoSystemDescriptor<U>,
{
self.world
.get_resource::<BackrollStages>()
.expect("No BackrollStages found! Did you install the plugin?")
.0
.lock()
.simulate
.add_system(system);
self
}
fn add_rollback_system_set(&mut self, system: impl Into<SystemSet>) -> &mut Self {
self.world
.get_resource::<BackrollStages>()
.expect("No BackrollStages found! Did you install the plugin?")
.0
.lock()
.simulate
.add_system_set(system.into());
self
}
}
pub trait BackrollCommands {
fn start_backroll_session<Input>(&mut self, session: P2PSession<Input>)
where
Input: PartialEq + bytemuck::Pod + bytemuck::Zeroable + Send + Sync;
fn end_backroll_session<Input>(&mut self)
where
Input: PartialEq + bytemuck::Pod + bytemuck::Zeroable + Send + Sync;
}
impl<'w, 's> BackrollCommands for BevyCommands<'w, 's> {
fn start_backroll_session<Input>(&mut self, session: P2PSession<Input>)
where
Input: PartialEq + bytemuck::Pod + bytemuck::Zeroable + Send + Sync,
{
self.insert_resource(NetworkIdProvider::new());
self.insert_resource(session);
}
fn end_backroll_session<Input>(&mut self)
where
Input: PartialEq + bytemuck::Pod + bytemuck::Zeroable + Send + Sync,
{
self.remove_resource::<P2PSession<Input>>();
}
}