use crate::{DEFAULT_FPS, MaxPredictionWindow};
use bevy::{ecs::schedule::ScheduleLabel, platform::collections::HashMap, prelude::*};
use seahash::SeaHasher;
use std::{collections::VecDeque, marker::PhantomData};
mod checksum;
mod childof_snapshot;
mod component_checksum;
mod component_map;
mod component_snapshot;
mod despawn;
mod entity;
mod entity_checksum;
mod resource_checksum;
mod resource_map;
mod resource_snapshot;
mod rollback;
mod rollback_app;
mod rollback_entity_map;
mod set;
mod strategy;
pub use checksum::*;
pub use childof_snapshot::*;
pub use component_checksum::*;
pub use component_map::*;
pub use component_snapshot::*;
pub use despawn::*;
pub use entity::*;
pub use entity_checksum::*;
pub use resource_checksum::*;
pub use resource_map::*;
pub use resource_snapshot::*;
pub use rollback::*;
pub use rollback_app::*;
pub use rollback_entity_map::*;
pub use set::*;
pub use strategy::*;
pub mod prelude {
pub use super::despawn::{RollbackDespawnCommandExtension, RollbackDespawned};
pub use super::{Checksum, LoadWorldSystems, SaveWorldSystems};
}
#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone)]
pub struct LoadWorld;
#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone)]
pub struct SaveWorld;
#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone)]
pub struct AdvanceWorld;
#[derive(Resource, Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RollbackFrameCount(pub i32);
impl From<RollbackFrameCount> for i32 {
fn from(value: RollbackFrameCount) -> i32 {
value.0
}
}
#[derive(Resource, Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ConfirmedFrameCount(pub i32);
impl From<ConfirmedFrameCount> for i32 {
fn from(value: ConfirmedFrameCount) -> i32 {
value.0
}
}
pub type GgrsResourceSnapshots<R, As = R> = GgrsSnapshots<R, Option<As>>;
pub type GgrsComponentSnapshots<C, As = C> = GgrsSnapshots<C, GgrsComponentSnapshot<C, As>>;
#[derive(Resource)]
pub struct GgrsSnapshots<For, As = For> {
snapshots: VecDeque<As>,
frames: VecDeque<i32>,
depth: usize,
_phantom: PhantomData<For>,
}
impl<For, As> Default for GgrsSnapshots<For, As> {
fn default() -> Self {
Self {
snapshots: VecDeque::new(),
frames: VecDeque::new(),
depth: DEFAULT_FPS, _phantom: default(),
}
}
}
impl<For, As> GgrsSnapshots<For, As> {
pub fn set_depth(&mut self, depth: usize) -> &mut Self {
self.depth = depth;
if self.snapshots.capacity() < self.depth {
let additional = self.depth - self.snapshots.capacity();
self.snapshots.reserve(additional);
}
if self.frames.capacity() < self.depth {
let additional = self.depth - self.frames.capacity();
self.frames.reserve(additional);
}
self
}
pub const fn depth(&self) -> usize {
self.depth
}
pub fn push(&mut self, frame: i32, snapshot: As) -> &mut Self {
debug_assert_eq!(
self.snapshots.len(),
self.frames.len(),
"Snapshot and Frame queues must always be in sync"
);
loop {
let Some(¤t) = self.frames.front() else {
break;
};
let wrapped = current.abs_diff(frame) > u32::MAX / 2;
let current_after_frame = current >= frame && !wrapped;
let current_after_frame_wrapped = frame >= current && wrapped;
if current_after_frame || current_after_frame_wrapped {
self.snapshots.pop_front().unwrap();
self.frames.pop_front().unwrap();
} else {
break;
}
}
self.snapshots.push_front(snapshot);
self.frames.push_front(frame);
while self.snapshots.len() > self.depth {
self.snapshots.pop_back().unwrap();
self.frames.pop_back().unwrap();
}
self
}
pub fn confirm(&mut self, confirmed_frame: i32) -> &mut Self {
debug_assert_eq!(
self.snapshots.len(),
self.frames.len(),
"Snapshot and Frame queues must always be in sync"
);
while let Some(&frame) = self.frames.back() {
if frame < confirmed_frame {
self.snapshots.pop_back().unwrap();
self.frames.pop_back().unwrap();
} else {
break;
}
}
self
}
pub fn rollback(&mut self, frame: i32) -> &mut Self {
loop {
let Some(¤t) = self.frames.front() else {
panic!("Could not rollback to {frame}: no snapshot at that moment could be found.");
};
if current != frame {
self.snapshots.pop_front().unwrap();
self.frames.pop_front().unwrap();
} else {
break;
}
}
self
}
pub fn get(&self) -> &As {
self.snapshots
.front()
.expect("no snapshot available — call rollback(frame) before get()")
}
pub fn peek(&self, frame: i32) -> Option<&As> {
let (index, _) = self
.frames
.iter()
.enumerate()
.find(|&(_, &saved_frame)| saved_frame == frame)?;
self.snapshots.get(index)
}
pub fn discard_old_snapshots(
mut snapshots: ResMut<Self>,
confirmed_frame: Option<Res<ConfirmedFrameCount>>,
) where
For: Send + Sync + 'static,
As: Send + Sync + 'static,
{
let Some(confirmed_frame) = confirmed_frame else {
return;
};
snapshots.confirm(confirmed_frame.0);
}
pub fn sync_depth(mut snapshots: ResMut<Self>, max_prediction: Option<Res<MaxPredictionWindow>>)
where
For: Send + Sync + 'static,
As: Send + Sync + 'static,
{
let Some(max_prediction) = max_prediction else {
return;
};
snapshots.set_depth(max_prediction.0);
}
}
pub struct GgrsComponentSnapshot<For, As = For> {
snapshot: HashMap<RollbackId, As>,
_phantom: PhantomData<For>,
}
impl<For, As> Default for GgrsComponentSnapshot<For, As> {
fn default() -> Self {
Self {
snapshot: default(),
_phantom: default(),
}
}
}
impl<For, As> GgrsComponentSnapshot<For, As> {
pub fn new(components: impl IntoIterator<Item = (RollbackId, As)>) -> Self {
Self {
snapshot: components.into_iter().collect(),
..default()
}
}
pub fn insert(&mut self, entity: RollbackId, snapshot: As) -> &mut Self {
self.snapshot.insert(entity, snapshot);
self
}
pub fn get(&self, entity: &RollbackId) -> Option<&As> {
self.snapshot.get(entity)
}
pub fn iter(&self) -> impl Iterator<Item = (&RollbackId, &As)> + '_ {
self.snapshot.iter()
}
}
pub fn checksum_hasher() -> SeaHasher {
SeaHasher::new()
}
pub struct SnapshotPlugin;
impl Plugin for SnapshotPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(SnapshotSetPlugin)
.init_resource::<RollbackOrdered>()
.init_resource::<RollbackFrameCount>()
.init_resource::<ConfirmedFrameCount>()
.init_schedule(LoadWorld)
.init_schedule(SaveWorld)
.init_schedule(AdvanceWorld)
.add_plugins((
EntitySnapshotPlugin,
ResourceSnapshotPlugin::<CloneStrategy<RollbackOrdered>>::default(),
ChildOfSnapshotPlugin,
RollbackDespawnPlugin,
));
}
}
#[cfg(test)]
pub(crate) mod tests {
use bevy::prelude::*;
use super::{AdvanceWorld, GgrsSnapshots, LoadWorld, RollbackFrameCount, SaveWorld};
type Snap = GgrsSnapshots<u32, u32>;
fn snap_with_depth(depth: usize) -> Snap {
let mut s = Snap::default();
s.set_depth(depth);
s
}
#[test]
fn push_evicts_oldest_when_depth_exceeded() {
let mut s = snap_with_depth(3);
for i in 0..5_i32 {
s.push(i, i as u32);
}
assert!(s.peek(0).is_none());
assert!(s.peek(1).is_none());
assert_eq!(s.peek(2), Some(&2));
assert_eq!(s.peek(3), Some(&3));
assert_eq!(s.peek(4), Some(&4));
}
#[test]
fn push_older_frame_discards_newer() {
let mut s = snap_with_depth(8);
s.push(5, 50);
s.push(6, 60);
s.push(7, 70);
s.push(5, 99);
assert_eq!(s.peek(5), Some(&99));
assert!(s.peek(6).is_none());
assert!(s.peek(7).is_none());
}
#[test]
fn push_same_frame_replaces() {
let mut s = snap_with_depth(8);
s.push(3, 10);
s.push(3, 20);
assert_eq!(s.peek(3), Some(&20));
}
#[test]
fn confirm_prunes_older_frames() {
let mut s = snap_with_depth(8);
for i in 0..6_i32 {
s.push(i, i as u32);
}
s.confirm(3);
assert!(s.peek(0).is_none());
assert!(s.peek(1).is_none());
assert!(s.peek(2).is_none());
assert_eq!(s.peek(3), Some(&3));
assert_eq!(s.peek(4), Some(&4));
assert_eq!(s.peek(5), Some(&5));
}
#[test]
fn confirm_beyond_all_frames_empties_storage() {
let mut s = snap_with_depth(8);
for i in 0..4_i32 {
s.push(i, i as u32);
}
s.confirm(100);
for i in 0..4_i32 {
assert!(s.peek(i).is_none());
}
}
#[test]
fn confirm_on_empty_does_not_panic() {
let mut s: Snap = snap_with_depth(8);
s.confirm(5); }
#[test]
fn rollback_to_existing_frame() {
let mut s = snap_with_depth(8);
for i in 0..5_i32 {
s.push(i, i as u32 * 10);
}
s.rollback(2);
assert_eq!(s.get(), &20);
}
#[test]
fn rollback_discards_newer_frames() {
let mut s = snap_with_depth(8);
for i in 0..5_i32 {
s.push(i, i as u32);
}
s.rollback(2);
assert!(s.peek(3).is_none());
assert!(s.peek(4).is_none());
assert_eq!(s.peek(2), Some(&2));
}
#[test]
#[should_panic(expected = "Could not rollback to 99")]
fn rollback_missing_frame_panics() {
let mut s = snap_with_depth(8);
s.push(0, 0);
s.rollback(99);
}
#[test]
fn push_wraps_i32_max_to_min_retains_history() {
let mut s = snap_with_depth(8);
s.push(i32::MAX - 2, 1);
s.push(i32::MAX - 1, 2);
s.push(i32::MAX, 3);
s.push(i32::MIN, 4);
assert_eq!(s.peek(i32::MAX - 2), Some(&1));
assert_eq!(s.peek(i32::MAX - 1), Some(&2));
assert_eq!(s.peek(i32::MAX), Some(&3));
assert_eq!(s.peek(i32::MIN), Some(&4));
}
#[test]
fn push_max_after_min_evicts_min_as_future() {
let mut s = snap_with_depth(8);
s.push(i32::MIN, 1);
s.push(i32::MAX, 2);
assert!(
s.peek(i32::MIN).is_none(),
"i32::MIN should be evicted as a future frame"
);
assert_eq!(s.peek(i32::MAX), Some(&2));
}
pub(crate) fn save_world(world: &mut World) {
world.run_schedule(SaveWorld);
}
pub(crate) fn advance_frame(world: &mut World) -> i32 {
let mut frame_count = world
.get_resource_mut::<RollbackFrameCount>()
.expect("Unable to find GGRS RollbackFrameCount. Did you remove it?");
frame_count.0 += 1;
let frame = frame_count.0;
world.run_schedule(AdvanceWorld);
frame
}
pub(crate) fn load_world(world: &mut World, frame: i32) {
world
.get_resource_mut::<RollbackFrameCount>()
.expect("Unable to find GGRS RollbackFrameCount. Did you remove it?")
.0 = frame;
world.run_schedule(LoadWorld);
}
}