Skip to main content

fortress_rollback/network/
network_stats.rs

1use crate::Frame;
2
3/// The `NetworkStats` struct contains statistics about the current session.
4#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
5#[must_use = "NetworkStats should be inspected or used after being queried"]
6pub struct NetworkStats {
7    /// The length of the queue containing UDP packets which have not yet been acknowledged by the end client.
8    /// The length of the send queue is a rough indication of the quality of the connection. The longer the send queue, the higher the round-trip time between the
9    /// clients. The send queue will also be longer than usual during high packet loss situations.
10    pub send_queue_len: usize,
11    /// The roundtrip packet transmission time as calculated by Fortress Rollback.
12    pub ping: u128,
13    /// The estimated bandwidth used between the two clients, in kilobits per second.
14    pub kbps_sent: usize,
15
16    /// The number of frames Fortress Rollback calculates that the local client is behind the remote client at this instant in time.
17    /// For example, if at this instant the current game client is running frame 1002 and the remote game client is running frame 1009,
18    /// this value will mostly likely roughly equal 7.
19    pub local_frames_behind: i32,
20    /// The same as [`local_frames_behind`], but calculated from the perspective of the remote player.
21    ///
22    /// [`local_frames_behind`]: #structfield.local_frames_behind
23    pub remote_frames_behind: i32,
24
25    // === Checksum/Desync Detection Fields ===
26    /// The most recent frame for which checksums were compared between peers.
27    ///
28    /// This is `None` if no checksum comparison has occurred yet (e.g., early
29    /// in the session or if desync detection is disabled).
30    pub last_compared_frame: Option<Frame>,
31
32    /// The local checksum at [`last_compared_frame`].
33    ///
34    /// This is the checksum computed locally from the saved game state at that frame.
35    /// Compare with [`remote_checksum`] to check for desync.
36    ///
37    /// [`last_compared_frame`]: #structfield.last_compared_frame
38    /// [`remote_checksum`]: #structfield.remote_checksum
39    pub local_checksum: Option<u128>,
40
41    /// The remote checksum at [`last_compared_frame`].
42    ///
43    /// This is the checksum received from the remote peer for that frame.
44    /// Compare with [`local_checksum`] to check for desync.
45    ///
46    /// [`last_compared_frame`]: #structfield.last_compared_frame
47    /// [`local_checksum`]: #structfield.local_checksum
48    pub remote_checksum: Option<u128>,
49
50    /// Whether checksums matched at the most recently compared frame.
51    ///
52    /// This is a convenience field derived from comparing [`local_checksum`]
53    /// and [`remote_checksum`]. It is `None` if no comparison has occurred.
54    ///
55    /// * `Some(true)` - Checksums match, peers are synchronized
56    /// * `Some(false)` - **DESYNC DETECTED** - game state has diverged
57    /// * `None` - No comparison available yet
58    ///
59    /// [`local_checksum`]: #structfield.local_checksum
60    /// [`remote_checksum`]: #structfield.remote_checksum
61    pub checksums_match: Option<bool>,
62}
63
64impl NetworkStats {
65    /// Creates a new `NetworkStats` instance with default values.
66    pub fn new() -> Self {
67        Self::default()
68    }
69}
70
71impl std::fmt::Display for NetworkStats {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        // Destructure to ensure all fields are included when new fields are added.
74        let Self {
75            send_queue_len,
76            ping,
77            kbps_sent,
78            local_frames_behind,
79            remote_frames_behind,
80            last_compared_frame,
81            local_checksum,
82            remote_checksum,
83            checksums_match,
84        } = self;
85
86        write!(
87            f,
88            "NetworkStats {{ ping: {}ms, queue: {}, kbps: {}, local_behind: {}, remote_behind: {}",
89            ping, send_queue_len, kbps_sent, local_frames_behind, remote_frames_behind
90        )?;
91
92        // Include checksum fields if any checksum data is available
93        if last_compared_frame.is_some()
94            || local_checksum.is_some()
95            || remote_checksum.is_some()
96            || checksums_match.is_some()
97        {
98            write!(f, ", last_compared_frame: ")?;
99            match last_compared_frame {
100                Some(frame) => write!(f, "{}", frame.as_i32())?,
101                None => write!(f, "None")?,
102            }
103
104            write!(f, ", local_checksum: ")?;
105            match local_checksum {
106                Some(cs) => write!(f, "0x{:016x}", cs)?,
107                None => write!(f, "None")?,
108            }
109
110            write!(f, ", remote_checksum: ")?;
111            match remote_checksum {
112                Some(cs) => write!(f, "0x{:016x}", cs)?,
113                None => write!(f, "None")?,
114            }
115
116            write!(f, ", checksums_match: ")?;
117            match checksums_match {
118                Some(true) => write!(f, "true")?,
119                Some(false) => write!(f, "false")?,
120                None => write!(f, "None")?,
121            }
122        }
123
124        write!(f, " }}")
125    }
126}
127
128#[cfg(test)]
129#[allow(
130    clippy::panic,
131    clippy::unwrap_used,
132    clippy::expect_used,
133    clippy::indexing_slicing
134)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_network_stats_default() {
140        let stats = NetworkStats::default();
141        assert_eq!(stats.send_queue_len, 0);
142        assert_eq!(stats.ping, 0);
143        assert_eq!(stats.kbps_sent, 0);
144        assert_eq!(stats.local_frames_behind, 0);
145        assert_eq!(stats.remote_frames_behind, 0);
146        assert_eq!(stats.last_compared_frame, None);
147        assert_eq!(stats.local_checksum, None);
148        assert_eq!(stats.remote_checksum, None);
149        assert_eq!(stats.checksums_match, None);
150    }
151
152    #[test]
153    fn test_network_stats_new() {
154        let stats = NetworkStats::new();
155        assert_eq!(stats.send_queue_len, 0);
156        assert_eq!(stats.ping, 0);
157        assert_eq!(stats.kbps_sent, 0);
158        assert_eq!(stats.local_frames_behind, 0);
159        assert_eq!(stats.remote_frames_behind, 0);
160        assert_eq!(stats.last_compared_frame, None);
161        assert_eq!(stats.local_checksum, None);
162        assert_eq!(stats.remote_checksum, None);
163        assert_eq!(stats.checksums_match, None);
164    }
165
166    #[test]
167    fn test_network_stats_debug() {
168        let stats = NetworkStats {
169            send_queue_len: 5,
170            ping: 100,
171            kbps_sent: 50,
172            local_frames_behind: 2,
173            remote_frames_behind: -1,
174            last_compared_frame: None,
175            local_checksum: None,
176            remote_checksum: None,
177            checksums_match: None,
178        };
179        let debug = format!("{:?}", stats);
180        assert!(debug.contains("NetworkStats"));
181        assert!(debug.contains('5'));
182        assert!(debug.contains("100"));
183        assert!(debug.contains("50"));
184    }
185
186    #[test]
187    fn test_network_stats_clone() {
188        let stats = NetworkStats {
189            send_queue_len: 10,
190            ping: 50,
191            kbps_sent: 100,
192            local_frames_behind: 3,
193            remote_frames_behind: -2,
194            last_compared_frame: Some(Frame::new(42)),
195            local_checksum: Some(12345),
196            remote_checksum: Some(12345),
197            checksums_match: Some(true),
198        };
199        let cloned = stats;
200        assert_eq!(cloned.send_queue_len, 10);
201        assert_eq!(cloned.ping, 50);
202        assert_eq!(cloned.kbps_sent, 100);
203        assert_eq!(cloned.local_frames_behind, 3);
204        assert_eq!(cloned.remote_frames_behind, -2);
205        assert_eq!(cloned.last_compared_frame, Some(Frame::new(42)));
206        assert_eq!(cloned.local_checksum, Some(12345));
207        assert_eq!(cloned.remote_checksum, Some(12345));
208        assert_eq!(cloned.checksums_match, Some(true));
209    }
210
211    #[test]
212    fn test_network_stats_negative_frames_behind() {
213        let stats = NetworkStats {
214            send_queue_len: 0,
215            ping: 0,
216            kbps_sent: 0,
217            local_frames_behind: -5,
218            remote_frames_behind: 5,
219            last_compared_frame: None,
220            local_checksum: None,
221            remote_checksum: None,
222            checksums_match: None,
223        };
224        assert_eq!(stats.local_frames_behind, -5);
225        assert_eq!(stats.remote_frames_behind, 5);
226    }
227
228    #[test]
229    fn test_network_stats_checksum_fields() {
230        let stats = NetworkStats {
231            send_queue_len: 0,
232            ping: 0,
233            kbps_sent: 0,
234            local_frames_behind: 0,
235            remote_frames_behind: 0,
236            last_compared_frame: Some(Frame::new(100)),
237            local_checksum: Some(0xDEAD_BEEF),
238            remote_checksum: Some(0xCAFE_BABE),
239            checksums_match: Some(false),
240        };
241        assert_eq!(stats.last_compared_frame, Some(Frame::new(100)));
242        assert_eq!(stats.local_checksum, Some(0xDEAD_BEEF));
243        assert_eq!(stats.remote_checksum, Some(0xCAFE_BABE));
244        assert_eq!(stats.checksums_match, Some(false));
245    }
246
247    // ==========================================================================
248    // Display Tests
249    // ==========================================================================
250
251    #[test]
252    fn test_network_stats_display_without_checksum() {
253        let stats = NetworkStats {
254            send_queue_len: 5,
255            ping: 100,
256            kbps_sent: 50,
257            local_frames_behind: 2,
258            remote_frames_behind: -1,
259            last_compared_frame: None,
260            local_checksum: None,
261            remote_checksum: None,
262            checksums_match: None,
263        };
264        let display = format!("{}", stats);
265        assert!(display.starts_with("NetworkStats {"));
266        assert!(display.contains("ping: 100ms"));
267        assert!(display.contains("queue: 5"));
268        assert!(display.contains("kbps: 50"));
269        assert!(display.contains("local_behind: 2"));
270        assert!(display.contains("remote_behind: -1"));
271        // Should not include checksum fields when all are None
272        assert!(!display.contains("local_checksum"));
273    }
274
275    #[test]
276    fn test_network_stats_display_with_checksum() {
277        let stats = NetworkStats {
278            send_queue_len: 3,
279            ping: 50,
280            kbps_sent: 100,
281            local_frames_behind: 0,
282            remote_frames_behind: 0,
283            last_compared_frame: Some(Frame::new(42)),
284            local_checksum: Some(0xDEAD_BEEF_CAFE_BABE),
285            remote_checksum: Some(0x1234_5678_9ABC_DEF0),
286            checksums_match: Some(true),
287        };
288        let display = format!("{}", stats);
289        assert!(display.contains("ping: 50ms"));
290        assert!(display.contains("last_compared_frame: 42"));
291        assert!(display.contains("local_checksum: 0xdeadbeefcafebabe"));
292        assert!(display.contains("remote_checksum: 0x123456789abcdef0"));
293        assert!(display.contains("checksums_match: true"));
294    }
295
296    #[test]
297    fn test_network_stats_display_checksum_mismatch() {
298        let stats = NetworkStats {
299            send_queue_len: 0,
300            ping: 0,
301            kbps_sent: 0,
302            local_frames_behind: 0,
303            remote_frames_behind: 0,
304            last_compared_frame: Some(Frame::new(100)),
305            local_checksum: Some(0xAAAA),
306            remote_checksum: Some(0xBBBB),
307            checksums_match: Some(false),
308        };
309        let display = format!("{}", stats);
310        assert!(display.contains("checksums_match: false"));
311    }
312
313    #[test]
314    fn test_network_stats_display_partial_checksum() {
315        // Test when only some checksum fields are set
316        let stats = NetworkStats {
317            send_queue_len: 0,
318            ping: 0,
319            kbps_sent: 0,
320            local_frames_behind: 0,
321            remote_frames_behind: 0,
322            last_compared_frame: Some(Frame::new(50)),
323            local_checksum: None,
324            remote_checksum: None,
325            checksums_match: None,
326        };
327        let display = format!("{}", stats);
328        // Should still include checksum section because last_compared_frame is Some
329        assert!(display.contains("last_compared_frame: 50"));
330        assert!(display.contains("local_checksum: None"));
331    }
332}