bevy_backroll 0.3.0

A Bevy engine integration plugin for the backroll rollback networking library.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
#![warn(missing_docs)]

//! A [Bevy](https://bevyengine.org) plugin that adds support for running
//! [Backroll](https://crates.io/crates/backroll) sessions.
//!
//! Installing the plugin:
//! ```rust no_run
//! use backroll::*;
//! use bytemuck::*;
//! use bevy::prelude::*;
//! use bevy_backroll::*;
//!
//! // Create your Backroll input type
//! #[repr(C)]
//! #[derive(Clone, Copy, Eq, PartialEq, Pod, Zeroable)]
//! pub struct PlayerInput {
//!    // Input data...
//!    pub buttons_pressed: u8,
//! }
//!
//! // Create your state. Must implement Clone.
//! #[derive(Component, Clone)]
//! pub struct PlayerState {
//!    pub handle: PlayerHandle,
//!    pub current_value: u64,
//! }
//!
//! // Sample input from the local player's controller.
//! fn sample_player_input(player: In<PlayerHandle>) -> PlayerInput {
//!    // Sample input data...
//!    PlayerInput {
//!       buttons_pressed: 1,
//!    }
//! }
//!
//! // Use input to advance the game simulation.
//! fn simulate_game(
//!   input: Res<GameInput<PlayerInput>>,
//!   mut query: Query<&mut PlayerState>
//! ) {
//!    for mut player in query.iter_mut() {
//!       if let Ok(input) = input.get(player.handle) {
//!          player.current_value += input.buttons_pressed as u64;
//!       }
//!    }
//! }
//!
//! fn main() {
//!     App::new()
//!         .add_plugin(BackrollPlugin)
//!         .register_rollback_input(sample_player_input.system())
//!         .register_rollback_component::<PlayerState>()
//!         .add_rollback_system(simulate_game)
//!         .run();
//! }
//! ```
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::*;

/// A [`P2PSession`] alias for bevy_backroll sessions. Uses [`BevyBackrollConfig`]
/// as the config type.
pub type P2PSession<Input> = backroll::P2PSession<BevyBackrollConfig<Input>>;

/// The [SystemLabel] used by the [BackrollStage] added by [BackrollPlugin].
///
/// [SystemLabel]: bevy_ecs::schedule::SystemLabel
/// [BackrollStage]: self::BackrollStage
/// [BackrollPlugin]: self::BackrollPlugin
#[derive(Debug, Clone, Eq, Hash, StageLabel, PartialEq)]
pub struct BackrollUpdate;

/// Manages when to inject frame stalls to keep in sync with remote players.
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 {
        // Linearly decay the cadence based on how many frames ahead
        // the is. This will result in fast initial catch up and then
        // slowly smooth out small hitches.
        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>>);

/// The Backroll config type for bevy_backroll sessions.
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;
}

/// A [Stage] that transparently runs and handles Backroll sessions.
///
/// Each time the stage runs, it will poll the Backroll session, sample local player
/// inputs for the session, then advance the frame.
///
/// The stage will automatically handle Backroll commands by doing the following:
///  
///  - [Command::Save]: Saves an immutable copy of the components and resoures from the
///    main app [`World`] into a save state.
///  - [Command::Load]: Loads a prior saved World state into the main app [`World`].
///  - [Command::AdvanceFrame]: Injects the provided [GameInput] as a resource then
///    runs all simulation based systems once (see: [add_rollback_system])
///  - [Command::Event]: Forwards all events to Bevy. Can be read out via [EventReader].
///    Automatically handles time synchronization by smoothly injecting stall frames when
///    ahead of remote players.
///
/// This stage is best used with a [FixedTimestep] run criteria to ensure that the systems
/// are running at a consistent rate on all players in the game.
///
/// This stage will only run when there is a [P2PSession] with the same [Config] parameter
/// registered as a resource within the running World. If the stage was added via
/// [BackrollPlugin], [BackrollCommands::start_backroll_session] and [BackrollCommands::end_backroll_session]
/// can be used to start or end a session.
///
/// [Stage]: bevy_ecs::schedule::Stage
/// [World]: bevy_ecs::world::World
/// [Command]: backroll::Command
/// [BackrollCommands]: self::BackrollCommands
/// [FixedTimestep]: bevy_core::FixedTimestep
/// [EventReader]: bevy_app::EventReader
/// [add_rollback_system]: self::BackrollApp::add_rollback_system
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);
                    // TODO(james7132): Find a way to hash the state here generically.
                    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) => {
                    // Insert input via Resource
                    *world.get_resource_mut::<GameInput<Input>>().unwrap() = inputs;
                    stage.0.lock().simulate.run(world);
                }
                Command::Event(evt) => {
                    debug!("Received Backroll Event: {:?}", evt);

                    // Update time sync stalls properly.
                    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 {
                // No ongoing session, don't run.
                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;
            }
        }
    }
}

/// A Bevy plugin that adds a [BackrollStage] to the app.
///
/// **Note:** This stage does not enforce any specific system execution order.
/// Users of this stage should ensure that their included systems have a strict
/// deterministic execution order, otherwise simulation may result in desyncs.
///
/// Also registers Backroll's [Event] as an event type, which the stage will
/// forward to Bevy.
///
/// If the feature is enabled, this will also register the associated transport
/// layer implementations for Steam.
///
/// [BackrolLStage]: self::BackrollStage
/// [Event]: backroll::Event
#[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);
    }
}

/// Extension trait for configuring [App]s using a [BackrollPlugin].
///
/// [App]: bevy_app::App
/// [BackrollPlugin]: self::BackrollPlugin
pub trait BackrollApp {
    /// Sets the input sampler system for Backroll. This is required. Backroll will
    /// not start without this being set.
    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;

    /// Registers a specific component type for saving into Backroll's save states.
    /// Any game simulation state stored in components should be registered here.
    fn register_rollback_component<T: Component + Clone>(&mut self) -> &mut Self;

    /// Registers a specific resource type for saving into Backroll's save states.
    /// Any game simulation state stored in resources should be registered here.
    fn register_rollback_resource<T: Clone + Send + Sync + 'static>(&mut self) -> &mut Self;

    /// Sets the [RunCriteria] for the [BackrollStage]. By default this uses a [FixedTimestep]
    /// set to 60 ticks per second.
    ///
    /// [RunCriteria]: bevy_ecs::schedule::RunCriteria
    /// [BackrollStage]: self::BackrollStage
    /// [FixedTimestep]: bevy_core::FixedTimestep
    fn with_rollback_run_criteria<Input, S>(&mut self, system: S) -> &mut Self
    where
        S: System<In = (), Out = ShouldRun>;

    /// Adds a system to the Backroll stage.
    fn add_rollback_system<S, U>(&mut self, system: S) -> &mut Self
    where
        S: IntoSystemDescriptor<U>;

    /// Adds a [SystemSet] to the BackrollStage.
    ///
    /// [SystemSet]: bevy_ecs::schedule::SystemSet
    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
    }
}

/// Extension trait for [Commands] to start and stop Backroll sessions.
///
/// [Commands]: bevy_ecs::system::Commands
pub trait BackrollCommands {
    /// Starts a new Backroll session. If one is already in progress, it will be replaced
    /// and the old session will be dropped. This will add the session as a resource, which
    /// can be accessed via [Res].
    ///
    /// [Res]: bevy_ecs::system::Res
    fn start_backroll_session<Input>(&mut self, session: P2PSession<Input>)
    where
        Input: PartialEq + bytemuck::Pod + bytemuck::Zeroable + Send + Sync;

    /// Ends the ongoing Backroll session. This will remove the associated resource and drop
    /// the session.
    ///
    /// Does nothing if there is no ongoing session.
    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,
    {
        // Reset the NetworkIdProvider on the start of a new session.
        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>>();
    }
}