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
//! # GGRS //! GGRS (good game rollback system) is a reimagination of the GGPO network SDK written in 100% safe Rust 🦀. It replaces the C-style callback API with a clearer control flow. #![forbid(unsafe_code)] // let us try //#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] use std::fmt::Display; use std::net::SocketAddr; pub use error::GGRSError; pub use frame_info::{GameInput, GameState}; pub use network::network_stats::NetworkStats; pub use sessions::p2p_session::P2PSession; pub use sessions::p2p_spectator_session::P2PSpectatorSession; pub use sessions::sync_test_session::SyncTestSession; pub use sync_layer::GameStateCell; pub(crate) mod error; pub(crate) mod frame_info; pub(crate) mod input_queue; pub(crate) mod sync_layer; pub(crate) mod time_sync; pub(crate) mod sessions { pub(crate) mod p2p_session; pub(crate) mod p2p_spectator_session; pub(crate) mod sync_test_session; } pub(crate) mod network { pub(crate) mod compression; pub(crate) mod network_stats; pub(crate) mod udp_msg; pub(crate) mod udp_protocol; pub(crate) mod udp_socket; } // ############# // # CONSTANTS # // ############# /// The maximum number of players allowed. Theoretically, higher player numbers should work, but are not well-tested. pub const MAX_PLAYERS: u32 = 4; /// The maximum number of frames GGRS will roll back. Every gamestate older than this is guaranteed to be correct if the players did not desync. pub const MAX_PREDICTION_FRAMES: u32 = 8; /// The maximum number of bytes the input of a single player can consist of. This corresponds to the size of `usize`. /// Higher values should be possible, but are not tested. pub const MAX_INPUT_BYTES: usize = 8; /// Internally, -1 represents no frame / invalid frame. pub const NULL_FRAME: i32 = -1; pub type Frame = i32; pub type PlayerHandle = usize; /// Defines the three types of players that GGRS considers: /// - local players, who play on the local device, /// - remote players, who play on other devices and /// - spectators, who are remote players that do not contribute to the game input. /// Both `Remote` and `Spectator` have a socket address associated with them. #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] pub enum PlayerType { /// This player plays on the local device. Local, /// This player plays on a remote device identified by the socket address. Remote(std::net::SocketAddr), /// This player spectates on a remote device identified by the socket address. They do not contribute to the game input. Spectator(std::net::SocketAddr), } impl Default for PlayerType { fn default() -> Self { Self::Local } } /// A session is always in one of these states. You can query the current state of a session via `current_state()`. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum SessionState { /// When initializing, you must add all necessary players and start the session to continue. Initializing, /// When synchronizing, the session attempts to establish a connection to the remote clients. Synchronizing, /// When running, the session has synchronized and is ready to take and transmit player input. Running, } /// Notifications that you can receive from the session. Handling them is up to the user. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum GGRSEvent { /// The session made progress in synchronizing. After `total` roundtrips, the session are synchronized. Synchronizing { player_handle: PlayerHandle, total: u32, count: u32, }, /// The session is now synchronized with the remote client. Synchronized { player_handle: PlayerHandle }, /// The remote client has disconnected. Disconnected { player_handle: PlayerHandle }, /// The session has not received packets from the remote client for some time and will disconnect the remote in `disconnect_timeout` ms. NetworkInterrupted { player_handle: PlayerHandle, disconnect_timeout: u128, }, /// Sent only after a `NetworkInterrupted` event, if communication with that player has resumed. NetworkResumed { player_handle: PlayerHandle }, /// Sent out if GGRS recommends skipping a few frames to let clients catch up. If you receive this, consider waiting `skip_frames` number of frames. WaitRecommendation { skip_frames: u32 }, } /// Requests that you can receive from the session. Handling them is mandatory. #[derive(Debug)] pub enum GGRSRequest { /// You should save the current gamestate in the `cell` provided to you. The given `frame` is a sanity check: The gamestate you save should be from that frame. SaveGameState { cell: GameStateCell, frame: Frame }, /// You should load the gamestate in the `cell` provided to you. LoadGameState { cell: GameStateCell }, /// You should advance the gamestate with the `inputs` provided to you. AdvanceFrame { inputs: Vec<GameInput> }, } impl Display for GGRSRequest { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { GGRSRequest::SaveGameState { .. } => write!(f, "SaveGameState"), GGRSRequest::LoadGameState { .. } => write!(f, "LoadGameState"), &GGRSRequest::AdvanceFrame { .. } => write!(f, "AdvanceFrame"), } } } /// Used to create a new `SyncTestSession`. During a sync test, GGRS will simulate a rollback every frame and resimulate the last n states, where n is the given `check_distance`. /// During a `SyncTestSession`, GGRS will simulate a rollback every frame and resimulate the last n states, where n is the given check distance. /// The resimulated checksums will be compared with the original checksums and report if there was a mismatch. /// Due to the decentralized nature of saving and loading gamestates, checksum comparisons can only be made if `check_distance` is 2 or higher. /// This is a great way to test if your system runs deterministically. After creating the session, add a local player, set input delay for them and then start the session. /// # Example /// /// ``` /// # use ggrs::GGRSError; /// # fn main() -> Result<(), GGRSError> { /// let check_distance : u32 = 7; /// let num_players : u32 = 2; /// let input_size : usize = std::mem::size_of::<u32>(); /// let mut sess = ggrs::start_synctest_session(num_players, input_size, check_distance)?; /// # Ok(()) /// # } /// ``` /// /// # Errors /// - Will return a `InvalidRequestError` if the number of players is higher than the allowed maximum (see `MAX_PLAYERS`). /// - Will return a `InvalidRequestError` if `input_size` is higher than the allowed maximum (see `MAX_INPUT_BYTES`). /// - Will return a `InvalidRequestError` if the `check_distance is` higher than or equal to `MAX_PREDICTION_FRAMES`. pub fn start_synctest_session( num_players: u32, input_size: usize, check_distance: u32, ) -> Result<SyncTestSession, GGRSError> { if num_players > MAX_PLAYERS { return Err(GGRSError::InvalidRequest); } if input_size > MAX_INPUT_BYTES { return Err(GGRSError::InvalidRequest); } if check_distance > MAX_PREDICTION_FRAMES - 1 { return Err(GGRSError::InvalidRequest); } Ok(SyncTestSession::new( num_players, input_size, check_distance, )) } /// Used to create a new `P2PSession` for players who participate on the game input. After creating the session, add local and remote players, /// set input delay for local players and then start the session. /// # Example /// /// ``` /// # use ggrs::GGRSError; /// # fn main() -> Result<(), GGRSError> { /// let local_port: u16 = 7777; /// let num_players : u32 = 2; /// let input_size : usize = std::mem::size_of::<u32>(); /// let mut sess = ggrs::start_p2p_session(num_players, input_size, local_port)?; /// # Ok(()) /// # } /// ``` /// /// # Errors /// - Will return a `InvalidRequest` if the number of players is higher than the allowed maximum (see `MAX_PLAYERS`). /// - Will return a `InvalidRequest` if `input_size` is higher than the allowed maximum (see `MAX_INPUT_BYTES`). /// - Will return a `InvalidRequest` if the `check_distance is` higher than the allowed maximum (see `MAX_PREDICTION_FRAMES`). /// - Will return `SocketCreationFailed` if the UPD socket could not be created. pub fn start_p2p_session( num_players: u32, input_size: usize, local_port: u16, ) -> Result<P2PSession, GGRSError> { if num_players > MAX_PLAYERS { return Err(GGRSError::InvalidRequest); } if input_size > MAX_INPUT_BYTES { return Err(GGRSError::InvalidRequest); } P2PSession::new(num_players, input_size, local_port) .map_err(|_| GGRSError::SocketCreationFailed) } /// Used to create a new `P2PSpectatorSession` for a spectator. /// The session will receive inputs from all players from the given host directly. /// # Example /// /// ``` /// # use std::net::SocketAddr; /// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// let local_port: u16 = 7777; /// let num_players : u32 = 2; /// let input_size : usize = std::mem::size_of::<u32>(); /// let host_addr: SocketAddr = "127.0.0.1:8888".parse()?; /// let mut sess = ggrs::start_p2p_spectator_session(num_players, input_size, local_port, host_addr)?; /// # Ok(()) /// # } /// ``` /// /// # Errors /// - Will return a `InvalidRequest` if the number of players is higher than the allowed maximum (see `MAX_PLAYERS`). /// - Will return a `InvalidRequest` if `input_size` is higher than the allowed maximum (see `MAX_INPUT_BYTES`). /// - Will return a `InvalidRequest` if the `check_distance is` higher than the allowed maximum (see `MAX_PREDICTION_FRAMES`). /// - Will return `SocketCreationFailed` if the UPD socket could not be created. pub fn start_p2p_spectator_session( num_players: u32, input_size: usize, local_port: u16, host_addr: SocketAddr, ) -> Result<P2PSpectatorSession, GGRSError> { if num_players > MAX_PLAYERS { return Err(GGRSError::InvalidRequest); } if input_size > MAX_INPUT_BYTES { return Err(GGRSError::InvalidRequest); } P2PSpectatorSession::new(num_players, input_size, local_port, host_addr) .map_err(|_| GGRSError::SocketCreationFailed) }