nimble_client/
lib.rs

1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/nimble-rust/nimble
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5
6/*!
7
8# Nimble Client 🕹
9**Nimble Client** is a Rust crate designed to manage networking tasks for multiplayer games.
10It handles downloading the complete game state from a host, managing participants by sending
11requests to the host, sending predicted inputs (steps) to the host for smoother gameplay, and
12receiving authoritative steps to ensure consistent game state.
13
14## Features
15
16- **Game State Downloading:** Fetch the entire game state from the host. 🗂️
17- **Participant Management:** Add and remove players by sending requests to the host. ➕➖
18- **Input Prediction:** Send predicted inputs (steps) to the host for reduced latency. 🔮
19- **Authoritative Step Handling:** Receive and apply authoritative steps from the host to
20    maintain game state consistency. 📥📤
21- **Metrics and Logging:** Built-in support for network metrics and logging to monitor and
22    debug client operations. 📊🛠️
23
24*/
25
26pub mod err;
27pub mod prelude;
28
29use crate::err::ClientError;
30use app_version::VersionProvider;
31use flood_rs::{BufferDeserializer, Deserialize, Serialize};
32use log::trace;
33use metricator::MinMaxAvg;
34use monotonic_time_rs::{Millis, MillisDuration};
35use network_metrics::{CombinedMetrics, NetworkMetrics};
36use nimble_client_logic::err::ClientLogicError;
37use nimble_client_logic::LocalIndex;
38use nimble_client_logic::{ClientLogic, ClientLogicPhase, LocalPlayer};
39use nimble_layer::NimbleLayer;
40use nimble_protocol::prelude::HostToClientCommands;
41use nimble_rectify::{Rectify, RectifyCallbacks};
42use nimble_step::Step;
43use nimble_step_map::StepMap;
44use std::cmp::min;
45use std::fmt::{Debug, Display};
46use tick_id::TickId;
47use time_tick::TimeTick;
48
49pub type MillisDurationRange = RangeToFactor<MillisDuration, MillisDuration>;
50
51pub struct RangeToFactor<V, F> {
52    range_min: V,
53    min_factor: F,
54    range_max: V,
55    max_factor: F,
56    factor: F,
57}
58
59impl<V: PartialOrd, F> RangeToFactor<V, F> {
60    pub const fn new(range_min: V, range_max: V, min_factor: F, factor: F, max_factor: F) -> Self {
61        Self {
62            range_min,
63            min_factor,
64            range_max,
65            max_factor,
66            factor,
67        }
68    }
69
70    #[inline]
71    pub fn get_factor(&self, input: &V) -> &F {
72        if input < &self.range_min {
73            &self.min_factor
74        } else if input > &self.range_max {
75            &self.max_factor
76        } else {
77            &self.factor
78        }
79    }
80}
81
82pub trait GameCallbacks<StepT: Display>:
83    RectifyCallbacks<StepMap<Step<StepT>>> + VersionProvider + BufferDeserializer
84{
85}
86
87impl<T, StepT> GameCallbacks<StepT> for T
88where
89    T: RectifyCallbacks<StepMap<Step<StepT>>> + VersionProvider + BufferDeserializer,
90    StepT: Display,
91{
92}
93
94#[derive(Debug, PartialEq, Eq)]
95pub enum ClientPhase {
96    Normal,
97    CanSendPredicted,
98}
99
100/// The main client structure handling datagram communication, participant management, and input (step) prediction.
101///
102/// The `Client` does not handle game logic directly but relies on external game logic
103/// provided through the `GameCallbacks` trait.
104pub struct Client<
105    GameT: GameCallbacks<StepT> + Debug,
106    StepT: Clone + Deserialize + Serialize + Debug + Display,
107> {
108    nimble_layer: NimbleLayer,
109    logic: ClientLogic<GameT, StepT>,
110    metrics: NetworkMetrics,
111    rectify: Rectify<GameT, StepMap<Step<StepT>>>,
112    authoritative_range_to_tick_duration_ms: RangeToFactor<u8, f32>,
113    authoritative_time_tick: TimeTick,
114    prediction_range_to_tick_duration_ms: RangeToFactor<i32, f32>,
115    pub prediction_time_tick: TimeTick,
116    max_prediction_count: usize,
117    last_need_prediction_count: u16,
118    phase: ClientPhase,
119    tick_duration_ms: MillisDuration,
120}
121
122impl<
123        StepT: Clone + Deserialize + Serialize + Debug + Display + Eq,
124        GameT: GameCallbacks<StepT> + Debug,
125    > Client<GameT, StepT>
126{
127    /// Creates a new `Client` instance with the given current time.
128    ///
129    /// # Arguments
130    ///
131    /// * `now` - The current time in milliseconds.
132    #[must_use]
133    pub fn new(now: Millis) -> Self {
134        let deterministic_app_version = GameT::version();
135        Self {
136            nimble_layer: NimbleLayer::new(),
137            logic: ClientLogic::<GameT, StepT>::new(deterministic_app_version),
138            metrics: NetworkMetrics::new(now),
139
140            authoritative_range_to_tick_duration_ms: RangeToFactor::new(2, 5, 0.9, 1.0, 2.0), // 0.9 is faster, since it is a multiplier to tick_duration
141            authoritative_time_tick: TimeTick::new(now, MillisDuration::from_millis(16), 4),
142
143            prediction_range_to_tick_duration_ms: RangeToFactor::new(-1, 3, 0.85, 1.0, 2.0), // 0.9 is faster, since it is a multiplier to tick_duration
144            prediction_time_tick: TimeTick::new(now, MillisDuration::from_millis(16), 4),
145
146            rectify: Rectify::default(),
147            last_need_prediction_count: 0,
148            phase: ClientPhase::Normal,
149            max_prediction_count: 10, // TODO: Settings
150            tick_duration_ms: MillisDuration::from_millis(16),
151        }
152    }
153
154    #[must_use]
155    pub const fn with_tick_duration(mut self, tick_duration: MillisDuration) -> Self {
156        self.tick_duration_ms = tick_duration;
157        self
158    }
159
160    const MAX_DATAGRAM_SIZE: usize = 1024;
161
162    /// Creates outgoing messages and returns the serialized datagrams.
163    ///
164    /// This method collects messages prepared by the client logic, serializes them into datagrams,
165    /// updates network metrics, and returns the datagrams. They are usually sent over some datagram transport.
166    ///
167    /// # Arguments
168    ///
169    /// * `now` - The current time in milliseconds.
170    ///
171    /// # Returns
172    ///
173    /// A `Result` containing a vector of serialized datagrams or a `ClientError`.
174    ///
175    /// # Errors
176    ///
177    /// Returns `ClientError` if serialization or sending fails.
178    pub fn send(&mut self, now: Millis) -> Result<Vec<Vec<u8>>, ClientError> {
179        let messages = self.logic.send(now);
180        let datagrams =
181            datagram_chunker::serialize_to_datagrams(messages, Self::MAX_DATAGRAM_SIZE)?;
182        self.metrics.sent_datagrams(&datagrams);
183
184        let datagrams_with_header = self.nimble_layer.send(&datagrams)?;
185
186        Ok(datagrams_with_header)
187    }
188
189    /// Receives and processes an incoming datagram.
190    ///
191    /// This method handles incoming datagrams by updating metrics, deserializing the datagram,
192    /// and passing the contained commands to the client logic for further processing.
193    ///
194    /// # Arguments
195    ///
196    /// * `millis` - The current time in milliseconds.
197    /// * `datagram` - The received datagram bytes.
198    ///
199    /// # Returns
200    ///
201    /// A `Result` indicating success or containing a `ClientError`.
202    ///
203    /// # Errors
204    ///
205    /// Returns `ClientError` if deserialization or processing fails.
206    pub fn receive(&mut self, now: Millis, datagram: &[u8]) -> Result<(), ClientError> {
207        self.metrics.received_datagram(datagram);
208        let datagram_without_header = self.nimble_layer.receive(datagram)?;
209        let commands = datagram_chunker::deserialize_datagram::<HostToClientCommands<Step<StepT>>>(
210            datagram_without_header,
211        )?;
212        for command in commands {
213            self.logic.receive(now, &command)?;
214        }
215
216        Ok(())
217    }
218
219    pub const fn debug_rectify(&self) -> &Rectify<GameT, StepMap<Step<StepT>>> {
220        &self.rectify
221    }
222
223    /// Updates the client's phase and handles synchronization tasks based on the current time.
224    ///
225    /// This includes updating the network layer, metrics, tick durations, processing authoritative steps,
226    /// and managing prediction phases.
227    ///
228    /// # Arguments
229    ///
230    /// * `now` - The current time in milliseconds.
231    ///
232    /// # Returns
233    ///
234    /// A `Result` indicating success or containing a `ClientError`.
235    ///
236    /// # Errors
237    ///
238    /// Returns `ClientError` if any internal operations fail.
239    pub fn update(&mut self, now: Millis) -> Result<(), ClientError> {
240        trace!("client: update {now}");
241        self.metrics.update_metrics(now);
242
243        let factor = self.authoritative_range_to_tick_duration_ms.get_factor(
244            &u8::try_from(self.logic.debug_authoritative_steps().len())
245                .map_err(|_| ClientLogicError::TooManyAuthoritativeSteps)?,
246        );
247        self.authoritative_time_tick
248            .set_tick_duration(*factor * self.tick_duration_ms);
249        self.authoritative_time_tick.calculate_ticks(now);
250
251        let (first_tick_id_in_vector, auth_steps) = self.logic.pop_all_authoritative_steps();
252        let mut current_tick_id = first_tick_id_in_vector;
253        for auth_step in auth_steps {
254            if current_tick_id == self.rectify.waiting_for_authoritative_tick_id() {
255                self.rectify
256                    .push_authoritative_with_check(current_tick_id, auth_step)?;
257            }
258            current_tick_id = TickId(current_tick_id.0 + 1);
259        }
260
261        if self.logic.phase() == &ClientLogicPhase::SendPredictedSteps
262            && self.phase != ClientPhase::CanSendPredicted
263        {
264            self.prediction_time_tick.reset(now);
265            self.phase = ClientPhase::CanSendPredicted;
266        }
267
268        match self.phase {
269            ClientPhase::Normal => {}
270            ClientPhase::CanSendPredicted => {
271                self.adjust_prediction_ticker();
272                self.last_need_prediction_count = self.prediction_time_tick.calculate_ticks(now);
273                if self.logic.predicted_step_count_in_queue() >= self.max_prediction_count {
274                    trace!(
275                        "prediction queue is maxed out: {}",
276                        self.max_prediction_count
277                    );
278                    self.last_need_prediction_count = 0;
279                    self.prediction_time_tick.reset(now);
280                }
281
282                trace!("prediction count: {}", self.last_need_prediction_count);
283                if let Some(game) = self.logic.game_mut() {
284                    self.rectify.update(game);
285                }
286            }
287        }
288
289        Ok(())
290    }
291
292    /// Calculates the difference between the current prediction count and the optimal count.
293    ///
294    /// # Returns
295    ///
296    /// The difference as an `i32`. A positive value indicates excess predictions, while a negative
297    /// value indicates a deficit.
298    fn delta_prediction_count(&self) -> i32 {
299        if self.logic.can_push_predicted_step() {
300            let optimal_prediction_tick_count = self.optimal_prediction_tick_count();
301            let prediction_count_in_queue = self.logic.predicted_step_count_in_queue();
302            trace!("optimal according to latency {optimal_prediction_tick_count}, outgoing queue {prediction_count_in_queue}");
303
304            prediction_count_in_queue as i32 - optimal_prediction_tick_count as i32
305        } else {
306            0
307        }
308    }
309
310    /// Adjusts the prediction ticker based on the current delta prediction count.
311    fn adjust_prediction_ticker(&mut self) {
312        let delta_prediction = self.delta_prediction_count();
313        let factor = self
314            .prediction_range_to_tick_duration_ms
315            .get_factor(&delta_prediction);
316        trace!(
317            "delta-prediction: {delta_prediction} resulted in factor: {factor} for latency {}",
318            self.latency().unwrap_or(MinMaxAvg::new(0, 0.0, 0))
319        );
320
321        self.prediction_time_tick
322            .set_tick_duration(*factor * self.tick_duration_ms);
323    }
324
325    /// Determines the optimal number of prediction ticks based on current average latency.
326    ///
327    /// # Returns
328    ///
329    /// The optimal prediction tick count as a `usize`.
330    ///
331    /// # Notes
332    ///
333    /// This function ensures that the prediction count does not exceed a predefined maximum.
334    fn optimal_prediction_tick_count(&self) -> usize {
335        const MAXIMUM_PREDICTION_COUNT: usize = 10; // TODO: Setting
336        const MINIMUM_DELTA_TICK: u32 = 2;
337
338        self.latency().map_or(2, |latency_ms| {
339            let latency_in_ticks =
340                (latency_ms.avg as u16 / self.tick_duration_ms.as_millis() as u16) + 1;
341            let tick_delta = self.server_buffer_delta_ticks().unwrap_or(0);
342            let buffer_add = if (tick_delta as u32) < MINIMUM_DELTA_TICK {
343                ((MINIMUM_DELTA_TICK as i32) - i32::from(tick_delta)) as u32
344            } else {
345                0
346            };
347
348            let count = (u32::from(latency_in_ticks) + buffer_add) as usize;
349
350            min(count, MAXIMUM_PREDICTION_COUNT)
351        })
352    }
353
354    /// Retrieves a reference to the current game instance, if available.
355    ///
356    /// Note: The `Client` does not manage game logic directly. This method provides access to the
357    /// game state managed externally via callbacks.
358    ///
359    /// # Returns
360    ///
361    /// An `Option` containing a reference to `CallbacksT` or `None` if no game is active.
362    pub const fn game(&self) -> Option<&GameT> {
363        self.logic.game()
364    }
365
366    /// Determines the number of predictions needed based on the current state.
367    ///
368    /// # Returns
369    ///
370    /// The number of predictions needed as a `usize`.
371    pub fn required_prediction_count(&self) -> usize {
372        if self.logic.can_push_predicted_step() {
373            self.last_need_prediction_count as usize
374        } else {
375            0
376        }
377    }
378
379    /// Checks if a new player can join the game session.
380    ///
381    /// # Returns
382    ///
383    /// `true` if a player can join, `false` otherwise.
384    pub const fn can_join_player(&self) -> bool {
385        self.game().is_some()
386    }
387
388    /// Retrieves a list of local players currently managed by the client.
389    ///
390    /// # Returns
391    ///
392    /// A vector of `LocalPlayer` instances.
393    pub fn local_players(&self) -> Vec<LocalPlayer> {
394        self.logic.local_players()
395    }
396
397    /// Adds a predicted input (step) to the client's logic and rectification system.
398    ///
399    /// This method serializes predicted steps into datagrams (in the future) in upcoming `send()` function calls.
400    ///
401    /// # Arguments
402    ///
403    /// * `tick_id` - The tick identifier for the predicted step.
404    /// * `step` - The predicted step data.
405    ///
406    /// # Returns
407    ///
408    /// A `Result` indicating success or containing a `ClientError`.
409    ///
410    /// # Errors
411    ///
412    /// Returns `ClientError` if the prediction queue is full or if processing fails.
413    pub fn push_predicted_step(
414        &mut self,
415        tick_id: TickId,
416        step: &StepMap<StepT>,
417    ) -> Result<(), ClientError> {
418        let count = step.len();
419        if count > self.required_prediction_count() {
420            return Err(ClientError::PredictionQueueOverflow);
421        }
422        self.prediction_time_tick.performed_ticks(count as u16);
423
424        self.logic.push_predicted_step(tick_id, step.clone())?;
425
426        let mut seq_map = StepMap::<Step<StepT>>::new();
427
428        for (participant_id, step) in step {
429            seq_map.insert(*participant_id, Step::Custom(step.clone()))?;
430        }
431        self.rectify.push_predicted(tick_id, seq_map)?;
432
433        Ok(())
434    }
435
436    /// Retrieves the current transmission round trip latency metrics.
437    ///
438    /// # Returns
439    ///
440    /// An `Option` containing `MinMaxAvg<u16>` representing latency metrics, or `None` if unavailable.
441    pub fn latency(&self) -> Option<MinMaxAvg<u16>> {
442        self.logic.latency()
443    }
444
445    /// Retrieves the combined network metrics.
446    ///
447    /// # Returns
448    ///
449    /// A `CombinedMetrics` instance containing various network metrics.
450    pub fn metrics(&self) -> CombinedMetrics {
451        self.metrics.metrics()
452    }
453
454    /// Retrieves the delta ticks on the host for the incoming predicted steps
455    /// A negative means that the incoming buffer is too low, a larger positive number
456    /// means that the buffer is too big, and the prediction should slow down.
457    ///
458    /// # Returns
459    ///
460    /// An `Option` containing the delta ticks as `i16`, or `None` if unavailable.
461    pub fn server_buffer_delta_ticks(&self) -> Option<i16> {
462        self.logic.server_buffer_delta_ticks()
463    }
464
465    /// Requests to join a new player with the specified local indices.
466    ///
467    /// This method sends a request to the host to add new participants to the game session.
468    ///
469    /// # Arguments
470    ///
471    /// * `local_players` - A vector of `LocalIndex` representing the local players to join.
472    ///
473    /// # Returns
474    ///
475    /// A `Result` indicating success or containing a `ClientError`.
476    /// # Errors
477    ///
478    /// `ClientError` // TODO:
479    pub fn request_join_player(
480        &mut self,
481        local_players: Vec<LocalIndex>,
482    ) -> Result<(), ClientError> {
483        self.logic.set_joining_player(local_players);
484        Ok(())
485    }
486}