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
//! The `clocksync` module is responsible for managing the difference between the clocks on the
//! local and remote machine (aka client and server clocks), and for the client to estimate the
//! server's local time.
//!
//! The design of this module is currently suboptimal and could be improved:
//! - Only the client-side clocksyncing logic is located here. The server-side logic is located in
//!   the [`Server`](crate::server::Server) itself.
//! - This clocksync module serves double duty for also informing the client which `client_id` has
//!   been allocated to this client by the server.

use crate::{
    network_resource::{Connection, NetworkResource},
    world::World,
    Config,
};
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use tracing::{info, trace, warn};

/// A message sent between client and server to measure the number of seconds difference between
/// the clocks of the two machines.
///
/// 1. The client first sends this structure to the server, ignoring the
///    `server_seconds_since_startup` and `client_id` fields. The
///    `client_send_seconds_since_startup` records the client's local time that this message was
///    prepared and sent.
/// 2. The server sends back this structure to the client, preserving the same
///    `client_send_seconds_since_startup` value as it received, but populating the remaining
///    `client_id` and `server_seconds_since_startup` values. The `server_seconds_since_startup` is
///    the server's local time at which it prepared and sent this message back to the client.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClockSyncMessage {
    /// The time (in seconds) when the client side sends this `ClockSyncMessage` request to the
    /// server.
    pub client_send_seconds_since_startup: f64,

    /// The time (in seconds) when the server side sends this `ClockSyncMessage` back to the client
    /// as a reply to the client's request.
    pub server_seconds_since_startup: f64,

    /// The `client_id` that shouldn't really belong here, but is tagging along for the ride. This
    /// is set by the server as part of the server's reply as well, and is set to the identifier
    /// that the server has allocated for the client that has made the request.
    pub client_id: usize,
}

/// Client-side clock syncing logic. This is responsible for sending out clock synchronization
/// request messages to the server, and handling the server's responses. This also serves double
/// duty for determining what client identifier has been allocated by the server to this client.
/// For each server response, a sample of the time clock offset between the client and the server
/// is calculated, and a rolling average (ignoring outliers) of these samples is used to update the
/// effective clock offset that is used by the client once the effective clock offset deviates too
/// far from the rolling average.
#[derive(Debug)]
pub struct ClockSyncer {
    /// The difference in seconds between client's seconds_since_startup and server's
    /// seconds_since_startup, where a positive value refers that an earlier client time value
    /// corresponds to the same instant as a later server time value. Since servers start
    /// earlier than clients, this value should in theory always be positive. The value stored
    /// here is the "effective" offset that is used by the client, and may be up to date with
    /// the latest measurements. Before initialization, the value is `None`.
    server_seconds_offset: Option<f64>,

    /// A ring buffer of the latest measurements of the clock difference between client and server.
    server_seconds_offset_samples: VecDeque<f64>,

    seconds_since_last_request_sent: f64,

    /// An identifier issued by the server for us to identify ourselves from other clients. Used,
    /// for example, for issuing our player's commands to the server.
    client_id: Option<usize>,

    config: Config,
}

impl ClockSyncer {
    /// Create a new [`ClockSyncer`] with the given configuration parameters. The [`ClockSyncer`]
    /// will start off in a ["not ready" state](ClockSyncer::is_ready) until after multiple
    /// [`update`](ClockSyncer::update) calls. During the time when the [`ClockSyncer`] is not
    /// ready, some of the methods may return `None` as documented.
    pub fn new(config: Config) -> Self {
        Self {
            server_seconds_offset: None,
            server_seconds_offset_samples: VecDeque::new(),
            seconds_since_last_request_sent: 0.0,
            client_id: None,
            config,
        }
    }

    /// Perform the next update, where the [`ClockSyncer`] tries to gather more information about
    /// the client-server clock differences and makes adjustments when needed.
    ///
    /// # Panics
    ///
    /// Panics when the [`ClockSyncer`] receives inconsistent `client_id` values from the server.
    pub fn update<WorldType: World, NetworkResourceType: NetworkResource<WorldType>>(
        &mut self,
        delta_seconds: f64,
        seconds_since_startup: f64,
        net: &mut NetworkResourceType,
    ) {
        self.seconds_since_last_request_sent += delta_seconds;
        if self.seconds_since_last_request_sent > self.config.clock_sync_request_period {
            self.seconds_since_last_request_sent = 0.0;

            trace!("Sending clocksync request");
            net.broadcast_message(ClockSyncMessage {
                client_send_seconds_since_startup: seconds_since_startup,
                server_seconds_since_startup: 0.0,
                client_id: 0,
            });
        }

        let mut latest_server_seconds_offset: Option<f64> = None;
        for (_, mut connection) in net.connections() {
            while let Some(sync) = connection.recv_clock_sync() {
                let received_time = seconds_since_startup;
                let corresponding_client_time =
                    (sync.client_send_seconds_since_startup + received_time) / 2.0;
                let offset = sync.server_seconds_since_startup - corresponding_client_time;

                trace!(
                    "Received clocksync response. ClientId: {}. Estimated clock offset: {}",
                    sync.client_id,
                    offset,
                );

                // Only one sample per update. Save the latest one.
                latest_server_seconds_offset = Some(offset);

                let existing_id = self.client_id.get_or_insert(sync.client_id);
                assert_eq!(*existing_id, sync.client_id);
            }
        }

        if let Some(measured_offset) = latest_server_seconds_offset {
            self.add_sample(measured_offset);
        }
    }

    /// Whether the [`ClockSyncer`] has enough information to make useful estimates.
    ///
    /// It is guaranteed that once the [`ClockSyncer`] becomes "ready", it stays "ready".
    ///
    /// TODO: Enforce this invariant.
    pub fn is_ready(&self) -> bool {
        self.server_seconds_offset.is_some() && self.client_id.is_some()
    }

    /// How many measurement samples the [`ClockSyncer`] has collected and currently stored.
    /// Previously collected samples that have since then been discarded are not counted. This
    /// merely counts the number of samples in the current moving window.
    pub fn sample_count(&self) -> usize {
        self.server_seconds_offset_samples.len()
    }

    /// How many samples are needed to start making useful estimates.
    pub fn samples_needed(&self) -> usize {
        self.config.clock_sync_samples_needed_to_store()
    }

    /// An identifier issued by the server for us to identify ourselves from other clients. Used,
    /// for example, for issuing our player's commands to the server.
    ///
    /// This is `None` if no server responses had been received yet.
    pub fn client_id(&self) -> &Option<usize> {
        &self.client_id
    }

    /// The difference in seconds between client's `seconds_since_startup` and server's
    /// `seconds_since_startup`, where a positive value refers that an earlier client time value
    /// corresponds to the same instant as a later server time value. Since servers start
    /// earlier than clients, this value should in theory always be positive. The value stored
    /// here is the "effective" offset that is used by the client, and may be up to date with
    /// the latest measurements.
    ///
    /// Before initialization, the value is `None`.
    pub fn server_seconds_offset(&self) -> &Option<f64> {
        &self.server_seconds_offset
    }

    /// Convert local client seconds since startup into server seconds since startup using the
    /// latest estimated time difference between the client and the server, if available.
    pub fn server_seconds_since_startup(&self, client_seconds_since_startup: f64) -> Option<f64> {
        self.server_seconds_offset
            .map(|offset| client_seconds_since_startup + offset)
    }

    fn add_sample(&mut self, measured_seconds_offset: f64) {
        self.server_seconds_offset_samples
            .push_back(measured_seconds_offset);

        assert!(
            self.server_seconds_offset_samples.len()
                <= self.config.clock_sync_samples_needed_to_store()
        );

        if self.server_seconds_offset_samples.len()
            >= self.config.clock_sync_samples_needed_to_store()
        {
            let rolling_mean_offset_seconds = self.rolling_mean_offset_seconds();

            let is_initial_sync = self.server_seconds_offset.is_none();
            let has_desynced = self.server_seconds_offset.map_or(false, |offset| {
                (rolling_mean_offset_seconds - offset).abs()
                    > self.config.max_tolerable_clock_deviation
            });

            if is_initial_sync || has_desynced {
                if is_initial_sync {
                    info!(
                        "Initial server seconds offset: {}",
                        rolling_mean_offset_seconds
                    );
                }
                if has_desynced {
                    warn!(
                        "Client updating clock offset from {:?} to {:?}",
                        self.server_seconds_offset, rolling_mean_offset_seconds,
                    );
                }
                self.server_seconds_offset = Some(rolling_mean_offset_seconds);
            }
            self.server_seconds_offset_samples.pop_front();
        }
    }

    fn rolling_mean_offset_seconds(&self) -> f64 {
        let mut samples: Vec<f64> = self.server_seconds_offset_samples.iter().copied().collect();
        samples.sort_unstable_by(|a, b| a.partial_cmp(b).expect("Samples should not be NaN"));

        let samples_without_outliers =
            &samples[self.config.clock_sync_samples_to_discard_per_extreme()
                ..samples.len() - self.config.clock_sync_samples_to_discard_per_extreme()];

        samples_without_outliers.iter().sum::<f64>() / samples_without_outliers.len() as f64
    }
}