Skip to content

Commit

Permalink
fix cross-platform desyncs (#1)
Browse files Browse the repository at this point in the history
* improve rng generator determinism

* determinism fixes in utils

* convert game usize/isize types to explicitly sized ones

* fix overflow in get_x/get_y methods

* use u8 instead of i8 in Position type
  • Loading branch information
aleksa2808 authored Dec 15, 2023
1 parent 0d29a5c commit 668406c
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 232 deletions.
18 changes: 17 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ crate-type = ["cdylib", "rlib"]

[dependencies]
rand = "0.8"
rand_xoshiro = "0.6"
itertools = "0.10"
bevy_ggrs = { version = "0.14.0" }
bytemuck = { version = "1.7", features = ["derive"] }
Expand Down Expand Up @@ -50,5 +51,5 @@ parking_lot = "0.12"
wasm-bindgen = "0.2"

[patch.crates-io]
bevy_ggrs = { git = "https://github.com/aleksa2808/bevy_ggrs" }
bevy_ggrs = { git = "https://github.com/aleksa2808/bevy_ggrs", branch = "desync_fixes" }
ggrs = { git = "https://github.com/aleksa2808/ggrs" }
47 changes: 21 additions & 26 deletions src/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,62 +44,57 @@ pub struct Player {

#[derive(Component, Clone, Copy)]
pub struct Dead {
pub cleanup_frame: usize,
pub cleanup_frame: u32,
}

#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Position {
pub y: isize,
pub x: isize,
pub y: u8,
pub x: u8,
}

impl Position {
pub fn offset(&self, direction: Direction, distance: usize) -> Self {
let distance = distance as isize;

let (y_offset, x_offset) = match direction {
Direction::Right => (0, distance),
Direction::Down => (distance, 0),
Direction::Left => (0, -distance),
Direction::Up => (-distance, 0),
pub fn offset(&self, direction: Direction, distance: u8) -> Self {
let (new_y, new_x) = match direction {
Direction::Right => (self.y, self.x + distance),
Direction::Down => (self.y + distance, self.x),
Direction::Left => (self.y, self.x - distance),
Direction::Up => (self.y - distance, self.x),
};

Position {
y: self.y + y_offset,
x: self.x + x_offset,
}
Position { y: new_y, x: new_x }
}
}

#[derive(Component, Clone, Copy, Hash)]
pub struct BombSatchel {
pub bombs_available: usize,
pub bomb_range: usize,
pub bombs_available: u8,
pub bomb_range: u8,
}

#[derive(Component, Clone, Copy)]
pub struct Bomb {
pub owner: Option<PlayerID>,
pub range: usize,
pub expiration_frame: usize,
pub range: u8,
pub expiration_frame: u32,
}

#[derive(Component, Clone, Copy)]
pub struct Moving {
pub direction: Direction,
pub next_move_frame: usize,
pub frame_interval: usize,
pub next_move_frame: u32,
pub frame_interval: u32,
}

#[derive(Component, Clone, Copy)]
pub struct Fuse {
pub color: Color,
pub start_frame: usize,
pub start_frame: u32,
}

#[derive(Component, Clone, Copy)]
pub struct Fire {
pub expiration_frame: usize,
pub expiration_frame: u32,
}

#[derive(Component, Clone, Copy)]
Expand All @@ -113,10 +108,10 @@ pub struct Destructible;

#[derive(Component, Clone, Copy)]
pub struct Crumbling {
pub expiration_frame: usize,
pub expiration_frame: u32,
}

#[derive(Component, Debug, Clone, Copy, Hash)]
#[derive(Component, Debug, Clone, Copy)]
pub enum Item {
BombsUp,
RangeUp,
Expand All @@ -125,5 +120,5 @@ pub enum Item {

#[derive(Component, Clone, Copy)]
pub struct BurningItem {
pub expiration_frame: usize,
pub expiration_frame: u32,
}
30 changes: 15 additions & 15 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ pub const COLORS: [RGBColor; 16] = [
RGBColor(242, 242, 242),
];

pub const PIXEL_SCALE: usize = 8;
pub const PIXEL_SCALE: u32 = 8;

pub const HUD_HEIGHT: usize = 14 * PIXEL_SCALE;
pub const HUD_HEIGHT: u32 = 14 * PIXEL_SCALE;

pub const TILE_HEIGHT: usize = 8 * PIXEL_SCALE;
pub const TILE_WIDTH: usize = 6 * PIXEL_SCALE;
pub const TILE_HEIGHT: u32 = 8 * PIXEL_SCALE;
pub const TILE_WIDTH: u32 = 6 * PIXEL_SCALE;

pub const WALL_Z_LAYER: f32 = 60.0;
pub const PLAYER_Z_LAYER: f32 = 50.0;
Expand All @@ -39,21 +39,21 @@ pub const INPUT_LEFT: u8 = 1 << 2;
pub const INPUT_RIGHT: u8 = 1 << 3;
pub const INPUT_ACTION: u8 = 1 << 4;

pub const ROUND_DURATION_SECS: usize = 60;
pub const ROUND_DURATION_SECS: u32 = 60;

pub const FPS: usize = 30;
pub const MAX_PREDICTED_FRAMES: usize = 8;
pub const FPS: u32 = 30;
pub const MAX_PREDICTED_FRAMES: u32 = 8;

// these must not be lower than MAX_PREDICTED_FRAMES
// TODO can some static asserts be made?
pub const GET_READY_DISPLAY_FRAME_COUNT: usize = 3 * FPS;
pub const GAME_START_FREEZE_FRAME_COUNT: usize = FPS / 2;
pub const LEADERBOARD_DISPLAY_FRAME_COUNT: usize = 2 * FPS;
pub const TOURNAMENT_WINNER_DISPLAY_FRAME_COUNT: usize = 5 * FPS;
pub const GET_READY_DISPLAY_FRAME_COUNT: u32 = 3 * FPS;
pub const GAME_START_FREEZE_FRAME_COUNT: u32 = FPS / 2;
pub const LEADERBOARD_DISPLAY_FRAME_COUNT: u32 = 2 * FPS;
pub const TOURNAMENT_WINNER_DISPLAY_FRAME_COUNT: u32 = 5 * FPS;

pub const BOMB_SHORTENED_FUSE_FRAME_COUNT: usize = 2;
pub const BOMB_SHORTENED_FUSE_FRAME_COUNT: u32 = 2;

pub const MOVING_OBJECT_FRAME_INTERVAL: usize = 1;
pub const MOVING_OBJECT_FRAME_INTERVAL: u32 = 1;

// TODO does float precision cause desyncs?
pub const ITEM_SPAWN_CHANCE_PERCENTAGE: usize = 33;
// TODO figure out if floats can be used deterministically
pub const ITEM_SPAWN_CHANCE_PERCENTAGE: u64 = 33;
11 changes: 8 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ pub fn run() {
let input_fn = native_input;

app.add_plugins(GgrsPlugin::<GgrsConfig>::default())
.set_rollback_schedule_fps(FPS)
.set_rollback_schedule_fps(FPS as usize)
.add_systems(ReadInputs, input_fn)
// Bevy components
.rollback_component_with_clone::<Sprite>()
Expand Down Expand Up @@ -141,11 +141,16 @@ pub fn run() {
.rollback_resource_with_copy::<FrameCount>()
.rollback_resource_with_copy::<WallOfDeath>()
.rollback_resource_with_copy::<GameFreeze>()
// checksums
.checksum_component_with_hash::<Player>()
.checksum_component_with_hash::<Position>()
.checksum_component_with_hash::<BombSatchel>()
.checksum_component_with_hash::<Item>()
// .checksum_resource_with_hash::<SessionRng>()
// enums seem to hash from an isize so the derived implementation isn't portable
.checksum_component::<Item>(|item| match item {
Item::BombsUp => 0,
Item::RangeUp => 1,
Item::BombPush => 2,
})
.add_systems(
GgrsSchedule,
// list too long for one chain
Expand Down
2 changes: 1 addition & 1 deletion src/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub struct Args {
pub room_id: String,

#[clap(long, short, default_value = "2")]
pub number_of_players: usize,
pub number_of_players: u8,
}

impl Default for Args {
Expand Down
58 changes: 35 additions & 23 deletions src/resources.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use bevy::{ecs as bevy_ecs, prelude::*, text::Font, utils::HashMap};
use bevy_matchbox::matchbox_socket::PeerId;
use rand::{rngs::StdRng, seq::IteratorRandom, Rng};
use rand::{Rng, SeedableRng};
use rand_xoshiro::Xoshiro256StarStar;

use crate::{
components::Position,
Expand All @@ -11,7 +12,7 @@ use crate::{
#[derive(Resource)]
pub struct NetworkStatsCooldown {
pub cooldown: Cooldown,
pub print_cooldown: usize,
pub print_cooldown: u32,
}

#[derive(Resource)]
Expand Down Expand Up @@ -92,7 +93,7 @@ impl GameTextures {
self.penguin_variants
.iter()
.cycle()
.nth(player_id.0)
.nth(player_id.0 as usize)
.unwrap()
}
}
Expand Down Expand Up @@ -148,11 +149,10 @@ impl FromWorld for GameTextures {

#[derive(Resource, Clone, Copy)]
pub struct MapSize {
pub rows: usize,
pub columns: usize,
pub rows: u8,
pub columns: u8,
}

// Not to be confused with Bevy ECS `World`!
#[derive(Resource, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(clippy::enum_variant_names)]
pub enum WorldType {
Expand All @@ -164,27 +164,27 @@ pub enum WorldType {
impl WorldType {
pub const LIST: [Self; 3] = [Self::GrassWorld, Self::IceWorld, Self::CloudWorld];

pub fn random(rng: &mut StdRng) -> Self {
match rng.gen_range(1..=3) {
1 => Self::GrassWorld,
2 => Self::IceWorld,
3 => Self::CloudWorld,
pub fn random(rng: &mut SessionRng) -> Self {
match rng.gen_u64() % 3 {
0 => Self::GrassWorld,
1 => Self::IceWorld,
2 => Self::CloudWorld,
_ => unreachable!(),
}
}

pub fn next_random(&self, rng: &mut StdRng) -> Self {
pub fn next_random(&self, rng: &mut SessionRng) -> Self {
Self::LIST
.into_iter()
.filter(|&w| w != *self)
.choose(rng)
.nth((rng.gen_u64() as usize) % (Self::LIST.len() - 1))
.unwrap()
}
}

#[derive(Resource)]
pub struct MatchboxConfig {
pub number_of_players: usize,
pub number_of_players: u8,
pub room_id: String,
pub matchbox_server_url: Option<String>,
pub ice_server_config: Option<ICEServerConfig>,
Expand All @@ -196,41 +196,53 @@ pub struct RngSeeds {
pub remote: HashMap<PeerId, Option<u64>>,
}

// I could not verify it but I assume that the Xoshiro256StarStar generator is platform-independent. This is necessary for cross-platform deterministic gameplay.
#[derive(Resource, Clone)]
pub struct SessionRng(pub StdRng);
pub struct SessionRng(Xoshiro256StarStar);

impl SessionRng {
pub fn new(seed: u64) -> Self {
Self(Xoshiro256StarStar::seed_from_u64(seed))
}

// Allow only `u64` number generation in order to prevent things like generating platform dependent `usize` values.
pub fn gen_u64(&mut self) -> u64 {
self.0.gen()
}
}

#[derive(Resource)]
pub struct LocalPlayerID(pub usize);
pub struct LocalPlayerID(pub u8);

#[derive(Resource)]
pub struct Leaderboard {
pub scores: HashMap<PlayerID, usize>,
pub winning_score: usize,
pub scores: HashMap<PlayerID, u8>,
pub winning_score: u8,
}

#[derive(Resource, Clone, Copy)]
pub struct FrameCount {
pub frame: usize,
pub frame: u32,
}

#[derive(Resource, Clone, Copy)]
pub enum WallOfDeath {
Dormant {
activation_frame: usize,
activation_frame: u32,
},
Active {
position: Position,
direction: Direction,
next_step_frame: usize,
next_step_frame: u32,
},
Done,
}

#[derive(Resource)]
pub struct GameEndFrame(pub usize);
pub struct GameEndFrame(pub u32);

#[derive(Resource, Clone, Copy)]
pub struct GameFreeze {
pub end_frame: usize,
pub end_frame: u32,
pub post_freeze_action: Option<PostFreezeAction>,
}
Loading

0 comments on commit 668406c

Please sign in to comment.