crystalorb 0.3.0

Network-agnostic, high-level game networking library
Documentation
#![feature(const_fn_floating_point_arithmetic)]
#![feature(map_first_last)]
#![feature(adt_const_params)]
#![feature(generic_associated_types)]
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![warn(clippy::pedantic, clippy::cargo, clippy::unwrap_used)]
#![allow(
    incomplete_features,
    clippy::must_use_candidate,
    clippy::redundant_feature_names, // Required because of feature names in Bevy turbulence plugin
    clippy::too_many_lines,
    clippy::module_name_repetitions,
    clippy::cast_lossless, // TODO: Fix these. See issue #1
    clippy::cast_sign_loss, // TODO: Fix these. See issue #1
    clippy::cast_possible_truncation, // TODO: Fix these. See issue #1
    clippy::cast_precision_loss, // TODO: Fix these. See issue #1
)]
#![doc = include_str!("../README.markdown")]

pub mod client;
pub mod clocksync;
pub mod command;
pub mod fixed_timestepper;
pub mod network_resource;
pub(crate) mod old_new;
pub mod server;
pub mod timestamp;
pub mod world;

/// Represent how to calculate the current client display state based on the simulated undershot
/// and overshot frames (since the two frames closest to the current time may not exactly line up
/// with the current time).
#[derive(Clone, Debug)]
pub enum TweeningMethod {
    /// Use the undershot frame if the simulated frames don't exactly line up to the current time.
    /// This is equivalent to linearly interpolating between the undershot and overshot frame, but
    /// using an interpolation paramater `t.floor()`.
    MostRecentlyPassed,

    /// Use the undershot frame or the overshot frame, whichever is closest to the current time.
    /// This is equivalent to interpolating between the undershot and overshot frame, but
    /// using an interpolation paramater `t.round()`.
    Nearest,

    /// Use the display state's interpolation function to find a suitable in-between display state
    /// between the undershot and overshot frame for the current time.
    Interpolated,
}

impl TweeningMethod {
    /// Depending on the tweening method, conditionally snap the interpolation parameter to 0.0 or
    /// 1.0.
    ///
    /// # Panics
    ///
    /// Asserts that `t` is within `0.0..=1.0`.
    pub fn shape_interpolation_t(&self, t: f64) -> f64 {
        assert!((0.0..=1.0).contains(&t));
        match self {
            TweeningMethod::MostRecentlyPassed => t.floor(),
            TweeningMethod::Nearest => t.round(),
            TweeningMethod::Interpolated => t,
        }
    }
}

/// Configuration parameters that tweak how CrystalOrb works.
///
/// For starters, you can just pass in the default values when you are creating the
/// [`client::Client`] and [`server::Server`] instances.
///
/// # Example
///
/// ```
/// use crystalorb::{Config, client::Client};
/// use crystalorb_demo::DemoWorld;
///
/// let client = Client::<DemoWorld>::new(Config::new());
/// ```
#[derive(Clone, Debug)]
pub struct Config {
    /// Maximum amount of client lag in seconds that the server will compensate for.
    /// A higher number allows a client with a high ping to be able to perform actions
    /// on the world at the correct time from the client's point of view.
    /// The server will only simulate `lag_compensation_latency` seconds in the past,
    /// and let each client predict `lag_compensation_latency` seconds ahead of the server
    /// using the command buffers.
    pub lag_compensation_latency: f64,

    /// When a client receives a snapshot update of the entire world from the server, the client
    /// uses this snapshot to update their simulation. However, immediately displaying the result
    /// of this update will have entities suddenly teleporting to their new destinations. Instead,
    /// we keep simulating the world without the new snapshot information, and slowly blend into the
    /// world with the new snapshot information. We linearly interpolate from the old and new
    /// worlds, taking `blend_latency` seconds before the user sees the entities in their
    /// updated locations.
    pub blend_latency: f64,

    /// The number of seconds that gets simulated with every [`World`](world::World)
    /// [`step`](fixed_timestepper::Stepper). This is the physics simulation "dt". This can be
    /// different to the `delta_seconds` between each
    /// [`Client::update`](client::Client::update)/[`Server::update`](server::Server::update) call.
    /// For more accurate and predictable physics simulations, you may want to have a smaller
    /// `timestep_seconds`. If your physics simulation is computationally intensive, you may want
    /// to have a larger `timestep_seconds`.
    pub timestep_seconds: f64,

    /// The number of of clock sync responses from the server before the client averages them
    /// to update the client's clock. The higher this number, the more resilient it is to
    /// jitter, but the longer it takes before the client becomes ready.
    pub clock_sync_needed_sample_count: usize,

    /// The assumed probability that the next clock_sync response sample is an outlier. Before
    /// the samples are averaged, outliers are ignored from the calculation. The number of
    /// samples to ignore is determined by this numebr, with a minimum of one sample ignored
    /// from each extreme (i.e. the max and the min sample gets ignored).
    pub clock_sync_assumed_outlier_rate: f64,

    /// This determines how rapid the client sends clock_sync requests to the server,
    /// represented as the number of seconds before sending the next request.
    pub clock_sync_request_period: f64,

    /// How big the difference needs to be between the following two:
    /// 1. the server clock offset value that the client is currently using for its
    ///    calculations, compared with
    /// 2. the current rolling average server clock offset that was measured using clock_sync
    ///    messages,
    /// before updating the clients.
    pub max_tolerable_clock_deviation: f64,

    /// How many seconds to wait before the server sends another snapshot out to the clients. The
    /// smaller this number, the higher the network traffic, but the sooner the client gets to
    /// correctly apply other clients' commands at the correct timestmp. Regarding the latter
    /// point, this is because commands take time to travel between clients, and by the time a
    /// client receives another client's command, the command's intended timestamp would have
    /// already been passed. The client can't rewind the world to reapply the command at the
    /// intended timestamp, so the best thing it can do is to apply at the current timestamp, and
    /// wait until the next server snapshot before finally applying the command at the intended
    /// timestamp during the snapshot fastforwarding process.
    pub snapshot_send_period: f64,

    /// This is the limit that CrystalOrb enforces to limit the number of times that the
    /// [`World`](world::World)'s [`step`](crate::fixed_timestepper::Stepper::step) function gets
    /// called per rendering frame (i.e. per [`Client::update`](crate::client::Client::update) or
    /// [`Server::update`](crate::server::Server::update)). This is to prevent a catastrophic
    /// situation where CrystalOrb could not keep up in one rendering frame, and making it worse in
    /// the next frame, and subsequently "freezing" up in a positive feedback loop.
    pub update_delta_seconds_max: f64,

    /// Due to floating-point rounding errors and due to the `update_delta_seconds_max` limit
    /// that CrystalOrb enforces, the simulation [`Timestamp`](timestamp::Timestamp) might
    /// slowly drift away from the intended value according to the system clock (which is a way
    /// to make sure that different clients on different machines stay in sync in terms of time).
    /// When this time drift is small, CrystalOrb can compensate it in the next update by running
    /// extra simulation frames or some fewer frames than usual. However, if the timestamp drift
    /// becomes too large, it is not possible to correct all of this drift in one update using this
    /// method, and instead we need to perform a "time-skip" without running the corresponding
    /// simulation steps. The threshold for this drift before we start skipping some simulation
    /// frames is defined by this configuration parameter.
    ///
    /// Large timestamp drifts could happen when, for example, the game client has some system
    /// lag. If the game client is hosted in the web browser, for exmple, then large timestamp
    /// drifts can occur when the browser tab goes out of focus and sleeps. Large timestamp
    /// drifts can also occur on the laptop when the computer enters sleep mode, etc.
    pub timestamp_skip_threshold_seconds: f64,

    /// When the [`Client`](client::Client) receives a [`Snapshot`](world::World::SnapshotType)
    /// from the [`Server`](server::Server), the snapshot is going to have a timestamp older than
    /// the current client simulation timestamp. Before the snapshot can be blended into the
    /// client, it needs to fastforwarded to the current timestamp by running more simulation frmes
    /// on the snapshot than on the world that is currently displayed on the client. This
    /// configuration parameter specifies how much faster the snapshot can be simulated compared
    /// with the existing client world during the fastforwarding process. A value of `2`, for
    /// example, would represent that the received server snapshot could only run at most `2`
    /// simulation steps every time the existing client world runs one step. In this scenario, if
    /// the server snapshot is `10` frames behind the client, then it would take `10` client frames
    /// before the server snapshot ctches up with the client.
    pub fastforward_max_per_step: usize,

    /// In crystalorb, the physics simulation is assumed to be running at a fixed timestep that is
    /// different to the rendering refresh rate. To suppress some forms of temporal aliasing due to
    /// these different timesteps, crystalorb allows the interpolate between the simulated physics
    /// frames to derive the displayed state.
    pub tweening_method: TweeningMethod,
}

impl Config {
    /// Returns a set of useable default configuration parameters.
    ///
    /// # Examples
    ///
    /// If you want to use the defaults:
    ///
    /// ```
    /// use crystalorb::{Config, client::Client};
    /// use crystalorb_demo::DemoWorld;
    ///
    /// let client = Client::<DemoWorld>::new(Config::new());
    /// ```
    ///
    /// If you want to override the defaults:
    ///
    /// ```
    /// use crystalorb::{Config, client::Client};
    /// use crystalorb_demo::DemoWorld;
    /// let client = Client::<DemoWorld>::new(Config {
    ///     lag_compensation_latency: 0.5,
    ///     ..Config::new()
    /// });
    /// ```
    ///
    /// You can use `Default::default()` too:
    ///
    /// ```
    /// use crystalorb::{Config, client::Client};
    /// use crystalorb_demo::DemoWorld;
    /// let client = Client::<DemoWorld>::new(Config {
    ///     lag_compensation_latency: 0.5,
    ///     ..Default::default()
    /// });
    /// ```
    ///
    /// Note that this is a constant function, so you can initialize your configuration as a
    /// constant.
    ///
    /// ```
    /// use crystalorb::Config;
    /// const CONFIG: Config = Config {
    ///     timestep_seconds: 1.0 / 24.0,
    ///     ..Config::new()
    /// };
    /// ```
    pub const fn new() -> Self {
        Self {
            lag_compensation_latency: 0.3,
            blend_latency: 0.2,
            timestep_seconds: 1.0 / 60.0,
            clock_sync_needed_sample_count: 8,
            clock_sync_request_period: 0.2,
            clock_sync_assumed_outlier_rate: 0.2,
            max_tolerable_clock_deviation: 0.1,
            snapshot_send_period: 0.1,
            update_delta_seconds_max: 0.25,
            timestamp_skip_threshold_seconds: 1.0,
            fastforward_max_per_step: 10,
            tweening_method: TweeningMethod::Interpolated,
        }
    }

    /// Re-expresses the amount of lag to compensate in terms of number of frames at the prescribed
    /// timestep.
    pub(crate) fn lag_compensation_frame_count(&self) -> i16 {
        (self.lag_compensation_latency / self.timestep_seconds).round() as i16
    }

    /// Re-expresses the speed of blending the server snapshot in terms of the amount that the
    /// interpolation parameter `t` needs to be incremented on each step.
    pub(crate) fn blend_progress_per_frame(&self) -> f64 {
        self.timestep_seconds / self.blend_latency
    }

    /// The number of samples N so that, before we calculate the rolling average, we skip the N
    /// lowest and N highest samples.
    pub(crate) fn clock_sync_samples_to_discard_per_extreme(&self) -> usize {
        (self.clock_sync_needed_sample_count as f64 * self.clock_sync_assumed_outlier_rate / 2.0)
            .max(1.0)
            .ceil() as usize
    }

    /// The total number of samples that need to be kept in the ring buffer, so that, after
    /// discarding the outliers, there is enough samples to calculate the rolling average.
    pub(crate) fn clock_sync_samples_needed_to_store(&self) -> usize {
        self.clock_sync_needed_sample_count + self.clock_sync_samples_to_discard_per_extreme() * 2
    }
}

impl Default for Config {
    fn default() -> Self {
        Self::new()
    }
}