#![warn(missing_docs)]
use backroll::{
command::{Command, Commands},
Config, Event, GameInput, PlayerHandle,
};
use bevy_app::{App, CoreStage, Plugin};
use bevy_ecs::{
event::Events,
prelude::*,
schedule::{IntoSystemDescriptor, ShouldRun, Stage, SystemSet, SystemStage},
system::{Commands as BevyCommands, System},
world::World,
};
use bevy_log::{debug, error};
use std::{
marker::PhantomData,
ops::{Deref, DerefMut},
};
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(Resource, Debug, Clone)]
pub struct BackrollInput<Input>(GameInput<Input>);
impl<Input> Deref for BackrollInput<Input> {
type Target = GameInput<Input>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<Input> DerefMut for BackrollInput<Input> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Resource, Clone)]
struct BackrollSession<Input: Send + Sync + bytemuck::Pod + PartialEq>(P2PSession<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
}
}
}
#[derive(Resource)]
struct BackrollStages {
save: SystemStage,
simulate: SystemStage,
before_load: SystemStage,
load: SystemStage,
run_criteria: Option<Box<dyn System<In = (), Out = ShouldRun>>>,
}
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) {
world.resource_scope(|world, mut stages: Mut<BackrollStages>| {
for command in commands {
match command {
Command::Save(save_state) => {
world.insert_resource(SaveStateBuilder::new());
stages.save.run(world);
save_state.save_without_hash(
world.remove_resource::<SaveStateBuilder>().unwrap().build(),
);
}
Command::Load(load_state) => {
world.insert_resource(load_state.load());
stages.before_load.run(world);
stages.load.run(world);
world.remove_resource::<SaveState>();
}
Command::AdvanceFrame(inputs) => {
*world.get_resource_mut::<BackrollInput<Input>>().unwrap() =
BackrollInput(inputs);
stages.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 = world.resource_scope(|world, mut stages: Mut<BackrollStages>| {
if let Some(ref mut run_criteria) = stages.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::<BackrollSession<Input>>()
{
session.0.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 {
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, Params>(&mut self, system: S) -> &mut Self
where
Input: PartialEq + bytemuck::Pod + bytemuck::Zeroable + Send + Sync,
S: IntoSystem<PlayerHandle, Input, Params> + Send + Sync + 'static;
fn register_rollback_component<T: Component + Clone>(&mut self) -> &mut Self;
fn register_rollback_resource<T: Resource + Clone>(&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, Params>(&mut self, system: S) -> &mut Self
where
Input: PartialEq + bytemuck::Pod + bytemuck::Zeroable + Send + Sync,
S: IntoSystem<PlayerHandle, Input, Params> + Send + Sync + 'static,
{
self.insert_resource(BackrollInput(GameInput::<Input>::default()));
self.add_stage_before(
CoreStage::Update,
BackrollUpdate,
BackrollStage::<Input> {
staller: FrameStaller::new(),
input_sample_fn: Box::new(S::into_system(system)),
},
);
self
}
fn register_rollback_component<T: Component + Clone>(&mut self) -> &mut Self {
let mut stages = self
.world
.get_resource_mut::<BackrollStages>()
.expect("No BackrollStages found! Did you install the plugin?");
stages.load.add_system(load_components::<T>);
stages.save.add_system(save_components::<T>);
self
}
fn register_rollback_resource<T: Resource + Clone>(&mut self) -> &mut Self {
let mut stages = self
.world
.get_resource_mut::<BackrollStages>()
.expect("No BackrollStages found! Did you install the plugin?");
stages.load.add_system(load_resource::<T>);
stages.save.add_system(save_resource::<T>);
self
}
fn with_rollback_run_criteria<Input, S>(&mut self, run_criteria: S) -> &mut Self
where
S: System<In = (), Out = ShouldRun>,
{
self.world
.get_resource_mut::<BackrollStages>()
.expect("No BackrollStages found! Did you install the plugin?")
.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_mut::<BackrollStages>()
.expect("No BackrollStages found! Did you install the plugin?")
.simulate
.add_system(system);
self
}
fn add_rollback_system_set(&mut self, system: impl Into<SystemSet>) -> &mut Self {
self.world
.get_resource_mut::<BackrollStages>()
.expect("No BackrollStages found! Did you install the plugin?")
.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(BackrollSession(session));
}
fn end_backroll_session<Input>(&mut self)
where
Input: PartialEq + bytemuck::Pod + bytemuck::Zeroable + Send + Sync,
{
self.remove_resource::<BackrollSession<Input>>();
}
}