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
#![feature(extended_key_value_attributes)]
#![feature(const_fn_floating_point_arithmetic)]
#![feature(map_first_last)]
#![feature(const_generics)]
#![feature(generic_associated_types)]
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![warn(clippy::pedantic, clippy::cargo, clippy::unwrap_used)]
#![allow(
    clippy::must_use_candidate,
    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()
    }
}