lightyear_link/
conditioner.rs

1//! Contains the `LinkConditioner` struct which can be used to simulate network conditions
2use bevy_reflect::Reflect;
3use core::time::Duration;
4use lightyear_core::time::Instant;
5use lightyear_utils::ready_buffer::ReadyBuffer;
6use rand::Rng;
7
8/// Contains configuration required to initialize a LinkConditioner
9#[derive(Clone, Debug, Default, Reflect)]
10pub struct LinkConditionerConfig {
11    /// Delay to receive incoming messages in milliseconds (half the RTT)
12    pub incoming_latency: Duration,
13    /// The maximum additional random latency to delay received incoming
14    /// messages in milliseconds. This may be added OR subtracted from the
15    /// latency determined in the `incoming_latency` property above
16    pub incoming_jitter: Duration,
17    /// The % chance that an incoming packet will be dropped.
18    /// Represented as a value between 0 and 1
19    pub incoming_loss: f32,
20}
21
22#[derive(Debug, Clone)]
23pub struct LinkConditioner<P: Eq> {
24    config: LinkConditionerConfig,
25    pub time_queue: ReadyBuffer<Instant, P>,
26}
27
28impl<P: Eq> LinkConditioner<P> {
29    pub fn new(config: LinkConditionerConfig) -> Self {
30        LinkConditioner {
31            config,
32            time_queue: ReadyBuffer::new(),
33        }
34    }
35
36    /// Add latency/jitter/loss to a packet
37    ///
38    /// `elapsed`: Duration since app start
39    pub(crate) fn condition_packet(&mut self, packet: P, instant: Instant) {
40        let mut rng = rand::rng();
41        if rng.random_range(0.0..1.0) <= self.config.incoming_loss {
42            return;
43        }
44        let mut latency: i32 = self.config.incoming_latency.as_millis() as i32;
45        let mut packet_timestamp = instant;
46        if self.config.incoming_jitter > Duration::default() {
47            let jitter: i32 = self.config.incoming_jitter.as_millis() as i32;
48            latency += rng.random_range(-jitter..jitter);
49        }
50        if latency > 0 {
51            packet_timestamp += Duration::from_millis(latency as u64);
52        }
53        self.time_queue.push(packet_timestamp, packet);
54    }
55
56    /// Check if a packet is ready to be returned
57    pub(crate) fn pop_packet(&mut self, instant: Instant) -> Option<P> {
58        self.time_queue.pop_item(&instant).map(|(_, packet)| packet)
59    }
60}
61
62impl LinkConditionerConfig {
63    /// Creates a new LinkConditionerConfig
64    pub fn new(incoming_latency: Duration, incoming_jitter: Duration, incoming_loss: f32) -> Self {
65        LinkConditionerConfig {
66            incoming_latency,
67            incoming_jitter,
68            incoming_loss,
69        }
70    }
71
72    /// Split the LinkConditionerConfig in half, returning two identical halves
73    pub fn half(self) -> Self {
74        LinkConditionerConfig {
75            incoming_latency: self.incoming_latency / 2,
76            incoming_jitter: self.incoming_jitter / 2,
77            incoming_loss: self.incoming_loss / 2.0,
78        }
79    }
80
81    /// Creates a new LinkConditioner that simulates a connection which is in a
82    /// good condition
83    pub fn good_condition() -> Self {
84        LinkConditionerConfig {
85            incoming_latency: Duration::from_millis(40),
86            incoming_jitter: Duration::from_millis(6),
87            incoming_loss: 0.002,
88        }
89    }
90
91    /// Creates a new `LinkConditioner` that simulates a connection which is in an
92    /// average condition
93    pub fn average_condition() -> Self {
94        LinkConditionerConfig {
95            incoming_latency: Duration::from_millis(100),
96            incoming_jitter: Duration::from_millis(15),
97            incoming_loss: 0.02,
98        }
99    }
100
101    /// Creates a new `LinkConditioner` that simulates a connection which is in an
102    /// poor condition
103    pub fn poor_condition() -> Self {
104        LinkConditionerConfig {
105            incoming_latency: Duration::from_millis(200),
106            incoming_jitter: Duration::from_millis(30),
107            incoming_loss: 0.10,
108        }
109    }
110}