Skip to main content

fortress_rollback/sessions/
p2p_spectator_session.rs

1use std::collections::{vec_deque::Drain, VecDeque};
2use std::fmt;
3use std::sync::Arc;
4
5use crate::{
6    frame_info::PlayerInput,
7    network::{
8        messages::ConnectionStatus,
9        protocol::{Event, UdpProtocol},
10    },
11    report_violation, safe_frame_add,
12    telemetry::{ViolationKind, ViolationObserver, ViolationSeverity},
13    Config, FortressError, FortressEvent, FortressRequest, Frame, InputStatus, InputVec,
14    InternalErrorKind, InvalidFrameReason, NetworkStats, NonBlockingSocket, PlayerHandle,
15    SessionState,
16};
17
18/// The number of frames the spectator advances in a single step during normal operation.
19///
20/// When not catching up to the host, spectators advance one frame at a time to maintain
21/// smooth playback. During catchup mode (when far behind), `catchup_speed` is used instead.
22const NORMAL_SPEED: usize = 1;
23
24/// [`SpectatorSession`] provides all functionality to connect to a remote host in a peer-to-peer fashion.
25///
26/// The host will broadcast all confirmed inputs to this session.
27/// This session can be used to spectate a session without contributing to the game input.
28pub struct SpectatorSession<T>
29where
30    T: Config,
31{
32    state: SessionState,
33    num_players: usize,
34    buffer_size: usize,
35    inputs: Vec<Vec<PlayerInput<T::Input>>>,
36    host_connect_status: Vec<ConnectionStatus>,
37    socket: Box<dyn NonBlockingSocket<T::Address>>,
38    host: UdpProtocol<T>,
39    event_queue: VecDeque<FortressEvent<T>>,
40    current_frame: Frame,
41    last_recv_frame: Frame,
42    max_frames_behind: usize,
43    catchup_speed: usize,
44    /// Optional observer for specification violations.
45    violation_observer: Option<Arc<dyn ViolationObserver>>,
46    /// Maximum number of events to queue before oldest are dropped.
47    max_event_queue_size: usize,
48}
49
50impl<T: Config> SpectatorSession<T> {
51    /// Creates a new [`SpectatorSession`] for a spectator.
52    /// The session will receive inputs from all players from the given host directly.
53    /// The session will use the provided socket.
54    #[allow(clippy::too_many_arguments)]
55    pub(crate) fn new(
56        num_players: usize,
57        socket: Box<dyn NonBlockingSocket<T::Address>>,
58        host: UdpProtocol<T>,
59        buffer_size: usize,
60        max_frames_behind: usize,
61        catchup_speed: usize,
62        violation_observer: Option<Arc<dyn ViolationObserver>>,
63        event_queue_size: usize,
64    ) -> Self {
65        // host connection status
66        let host_connect_status = vec![ConnectionStatus::default(); num_players];
67
68        // Use at least 1 for buffer size to prevent panics
69        let actual_buffer_size = buffer_size.max(1);
70
71        Self {
72            state: SessionState::Synchronizing,
73            num_players,
74            buffer_size: actual_buffer_size,
75            inputs: vec![
76                vec![PlayerInput::blank_input(Frame::NULL); num_players];
77                actual_buffer_size
78            ],
79            host_connect_status,
80            socket,
81            host,
82            event_queue: VecDeque::new(),
83            current_frame: Frame::NULL,
84            last_recv_frame: Frame::NULL,
85            max_frames_behind,
86            catchup_speed,
87            violation_observer,
88            max_event_queue_size: event_queue_size,
89        }
90    }
91
92    /// Returns the current [`SessionState`] of a session.
93    pub fn current_state(&self) -> SessionState {
94        self.state
95    }
96
97    /// Returns the number of frames behind the host
98    pub fn frames_behind_host(&self) -> usize {
99        let diff = self.last_recv_frame - self.current_frame;
100        // Gracefully handle the case where current_frame somehow exceeds last_recv_frame.
101        // This shouldn't happen in normal operation, but we report it and return 0 rather than panic.
102        if diff < 0 {
103            report_violation!(
104                ViolationSeverity::Warning,
105                ViolationKind::FrameSync,
106                "frames_behind_host: current_frame {} exceeds last_recv_frame {} - returning 0",
107                self.current_frame,
108                self.last_recv_frame
109            );
110            return 0;
111        }
112        diff as usize
113    }
114
115    /// Used to fetch some statistics about the quality of the network connection.
116    /// # Errors
117    /// - Returns [`NotSynchronized`] if the session is not connected to other clients yet.
118    ///
119    /// [`NotSynchronized`]: FortressError::NotSynchronized
120    #[must_use = "network stats should be inspected or logged"]
121    pub fn network_stats(&self) -> Result<NetworkStats, FortressError> {
122        self.host.network_stats()
123    }
124
125    /// Returns all events that happened since last queried for events. If the number of stored events exceeds `MAX_EVENT_QUEUE_SIZE`, the oldest events will be discarded.
126    pub fn events(&mut self) -> Drain<'_, FortressEvent<T>> {
127        self.event_queue.drain(..)
128    }
129
130    /// Returns a reference to the violation observer, if one was configured.
131    ///
132    /// This allows checking for violations that occurred during session operations
133    /// when using a [`CollectingObserver`] or similar.
134    ///
135    /// [`CollectingObserver`]: crate::telemetry::CollectingObserver
136    pub fn violation_observer(&self) -> Option<&Arc<dyn ViolationObserver>> {
137        self.violation_observer.as_ref()
138    }
139
140    /// You should call this to notify Fortress Rollback that you are ready to advance your gamestate by a single frame.
141    /// Returns an order-sensitive [`Vec<FortressRequest>`]. You should fulfill all requests in the exact order they are provided.
142    /// Failure to do so will cause panics later.
143    ///
144    /// # Errors
145    /// - Returns [`NotSynchronized`] if the session is not yet ready to accept input.
146    ///   In this case, you either need to start the session or wait for synchronization between clients.
147    ///
148    /// [`Vec<FortressRequest>`]: FortressRequest
149    /// [`NotSynchronized`]: FortressError::NotSynchronized
150    #[must_use = "FortressRequests must be processed to advance the game state"]
151    pub fn advance_frame(&mut self) -> Result<Vec<FortressRequest<T>>, FortressError> {
152        // receive info from host, trigger events and send messages
153        self.poll_remote_clients();
154
155        if self.state != SessionState::Running {
156            return Err(FortressError::NotSynchronized);
157        }
158
159        let frames_to_advance = if self.frames_behind_host() > self.max_frames_behind {
160            self.catchup_speed
161        } else {
162            NORMAL_SPEED
163        };
164
165        // Pre-allocate for the expected number of frames to advance.
166        // In normal operation this is 1, in catchup mode it's catchup_speed.
167        let mut requests = Vec::with_capacity(frames_to_advance);
168
169        for _ in 0..frames_to_advance {
170            // get inputs for the next frame
171            let frame_to_grab = safe_frame_add!(
172                self.current_frame,
173                1,
174                "SpectatorSession::advance_frames next"
175            );
176            let synced_inputs = self.inputs_at_frame(frame_to_grab)?;
177
178            requests.push(FortressRequest::AdvanceFrame {
179                inputs: synced_inputs,
180            });
181
182            // advance the frame, but only if grabbing the inputs succeeded
183            self.current_frame = safe_frame_add!(
184                self.current_frame,
185                1,
186                "SpectatorSession::advance_frames current"
187            );
188        }
189
190        Ok(requests)
191    }
192
193    /// Receive UDP packages, distribute them to corresponding UDP endpoints, handle all occurring events and send all outgoing UDP packages.
194    /// Should be called periodically by your application to give Fortress Rollback a chance to do internal work like packet transmissions.
195    pub fn poll_remote_clients(&mut self) {
196        // Get all udp packets and distribute them to associated endpoints.
197        // The endpoints will handle their packets, which will trigger both events and UPD replies.
198        for (from, msg) in &self.socket.receive_all_messages() {
199            if self.host.is_handling_message(from) {
200                self.host.handle_message(msg);
201            }
202        }
203
204        // run host poll and get events. This will trigger additional UDP packets to be sent.
205        let mut events = VecDeque::new();
206        let addr = self.host.peer_addr();
207        for event in self.host.poll(&self.host_connect_status) {
208            events.push_back((event, addr.clone()));
209        }
210
211        // handle all events locally
212        for (event, addr) in std::mem::take(&mut events) {
213            self.handle_event(event, addr);
214        }
215
216        // send out all pending UDP messages
217        self.host.send_all_messages(&mut self.socket);
218    }
219
220    /// Returns the current frame of a session.
221    pub fn current_frame(&self) -> Frame {
222        self.current_frame
223    }
224
225    /// Returns the number of players this session was constructed with.
226    pub fn num_players(&self) -> usize {
227        self.num_players
228    }
229
230    fn inputs_at_frame(&self, frame_to_grab: Frame) -> Result<InputVec<T::Input>, FortressError> {
231        // Validate frame is valid before computing index
232        if frame_to_grab.is_null() || frame_to_grab.as_i32() < 0 {
233            report_violation!(
234                ViolationSeverity::Error,
235                ViolationKind::FrameSync,
236                "inputs_at_frame called with invalid frame {:?}",
237                frame_to_grab
238            );
239            return Err(FortressError::InvalidFrameStructured {
240                frame: frame_to_grab,
241                reason: InvalidFrameReason::NullOrNegative,
242            });
243        }
244
245        let buffer_index = frame_to_grab.as_i32() as usize % self.buffer_size;
246        let player_inputs =
247            self.inputs
248                .get(buffer_index)
249                .ok_or(FortressError::InternalErrorStructured {
250                    kind: InternalErrorKind::BufferIndexOutOfBounds,
251                })?;
252
253        // We haven't received the input from the host yet. Wait.
254        let first_input = player_inputs
255            .first()
256            .ok_or(FortressError::InternalErrorStructured {
257                kind: InternalErrorKind::EmptyPlayerInputs,
258            })?;
259        if first_input.frame < frame_to_grab {
260            return Err(FortressError::PredictionThreshold);
261        }
262
263        // The host is more than buffer_size frames ahead of the spectator. The input we need is gone forever.
264        if first_input.frame > frame_to_grab {
265            return Err(FortressError::SpectatorTooFarBehind);
266        }
267
268        Ok(player_inputs
269            .iter()
270            .enumerate()
271            .map(|(handle, player_input)| {
272                if let Some(status) = self.host_connect_status.get(handle) {
273                    if status.disconnected && status.last_frame < frame_to_grab {
274                        (player_input.input, InputStatus::Disconnected)
275                    } else {
276                        (player_input.input, InputStatus::Confirmed)
277                    }
278                } else {
279                    // If we can't get the connection status, assume confirmed
280                    (player_input.input, InputStatus::Confirmed)
281                }
282            })
283            .collect())
284    }
285
286    fn handle_event(&mut self, event: Event<T>, addr: T::Address) {
287        match event {
288            // forward to user
289            Event::Synchronizing {
290                total,
291                count,
292                total_requests_sent,
293                elapsed_ms,
294            } => {
295                self.event_queue.push_back(FortressEvent::Synchronizing {
296                    addr,
297                    total,
298                    count,
299                    total_requests_sent,
300                    elapsed_ms,
301                });
302            },
303            // forward to user
304            Event::NetworkInterrupted { disconnect_timeout } => {
305                self.event_queue
306                    .push_back(FortressEvent::NetworkInterrupted {
307                        addr,
308                        disconnect_timeout,
309                    });
310            },
311            // forward to user
312            Event::NetworkResumed => {
313                self.event_queue
314                    .push_back(FortressEvent::NetworkResumed { addr });
315            },
316            // synced with the host, then forward to user
317            Event::Synchronized => {
318                self.state = SessionState::Running;
319                self.event_queue
320                    .push_back(FortressEvent::Synchronized { addr });
321            },
322            // disconnect the player, then forward to user
323            Event::Disconnected => {
324                self.event_queue
325                    .push_back(FortressEvent::Disconnected { addr });
326            },
327            // forward sync timeout to user
328            Event::SyncTimeout { elapsed_ms } => {
329                self.event_queue
330                    .push_back(FortressEvent::SyncTimeout { addr, elapsed_ms });
331            },
332            // add the input and all associated information
333            Event::Input { input, player } => {
334                // Validate frame before using as index - negative frames would wrap around
335                if input.frame.is_null() || input.frame.as_i32() < 0 {
336                    report_violation!(
337                        ViolationSeverity::Warning,
338                        ViolationKind::FrameSync,
339                        "Received input with invalid frame {:?} for player {} - ignoring",
340                        input.frame,
341                        player
342                    );
343                    return;
344                }
345
346                // Validate player handle is in bounds
347                if player.as_usize() >= self.num_players {
348                    report_violation!(
349                        ViolationSeverity::Warning,
350                        ViolationKind::InternalError,
351                        "Received input for player {} but only {} players configured - ignoring",
352                        player,
353                        self.num_players
354                    );
355                    return;
356                }
357
358                // save the input
359                let frame_index = input.frame.as_i32() as usize % self.buffer_size;
360                if let Some(frame_inputs) = self.inputs.get_mut(frame_index) {
361                    if let Some(player_input) = frame_inputs.get_mut(player.as_usize()) {
362                        *player_input = input;
363                    } else {
364                        report_violation!(
365                            ViolationSeverity::Warning,
366                            ViolationKind::InternalError,
367                            "Failed to store input for player {} at frame {} - player index out of bounds",
368                            player,
369                            input.frame
370                        );
371                        return;
372                    }
373                } else {
374                    report_violation!(
375                        ViolationSeverity::Warning,
376                        ViolationKind::InternalError,
377                        "Failed to store input at frame {} - frame index {} out of bounds",
378                        input.frame,
379                        frame_index
380                    );
381                    return;
382                }
383
384                // Validate frame ordering - should receive frames in order
385                if input.frame < self.last_recv_frame {
386                    report_violation!(
387                        ViolationSeverity::Warning,
388                        ViolationKind::FrameSync,
389                        "Received out-of-order input: frame {} is older than last_recv_frame {}",
390                        input.frame,
391                        self.last_recv_frame
392                    );
393                    // Still update if this is a newer frame than what we had
394                }
395                if input.frame > self.last_recv_frame {
396                    self.last_recv_frame = input.frame;
397                }
398
399                // update the frame advantage
400                self.host.update_local_frame_advantage(input.frame);
401
402                // update the host connection status
403                for i in 0..self.num_players {
404                    if let Some(status) = self.host_connect_status.get_mut(i) {
405                        *status = self.host.peer_connect_status(PlayerHandle::new(i));
406                    } else {
407                        report_violation!(
408                            ViolationSeverity::Warning,
409                            ViolationKind::InternalError,
410                            "Failed to update connection status for player {} - index out of bounds",
411                            i
412                        );
413                    }
414                }
415            },
416        }
417
418        // check event queue size and discard oldest events if too big
419        while self.event_queue.len() > self.max_event_queue_size {
420            self.event_queue.pop_front();
421        }
422    }
423}
424
425impl<T: Config> fmt::Debug for SpectatorSession<T> {
426    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427        f.debug_struct("SpectatorSession")
428            .field("state", &self.state)
429            .field("num_players", &self.num_players)
430            .field("current_frame", &self.current_frame)
431            .field("last_recv_frame", &self.last_recv_frame)
432            .field("buffer_size", &self.buffer_size)
433            .field("max_frames_behind", &self.max_frames_behind)
434            .field("catchup_speed", &self.catchup_speed)
435            .finish_non_exhaustive()
436    }
437}
438
439#[cfg(test)]
440#[allow(
441    clippy::panic,
442    clippy::unwrap_used,
443    clippy::expect_used,
444    clippy::indexing_slicing,
445    clippy::needless_collect
446)]
447mod tests {
448    use super::*;
449    use crate::{Config, Message, NonBlockingSocket, SessionBuilder};
450    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
451
452    /// A minimal test configuration for unit testing.
453    struct TestConfig;
454
455    impl Config for TestConfig {
456        type Input = u8;
457        type State = u8;
458        type Address = SocketAddr;
459    }
460
461    fn test_addr(port: u16) -> SocketAddr {
462        SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)
463    }
464
465    /// A dummy socket that doesn't actually send or receive messages.
466    /// Used for unit testing without network dependencies.
467    struct DummySocket;
468
469    impl NonBlockingSocket<SocketAddr> for DummySocket {
470        fn send_to(&mut self, _msg: &Message, _addr: &SocketAddr) {}
471        fn receive_all_messages(&mut self) -> Vec<(SocketAddr, Message)> {
472            Vec::new()
473        }
474    }
475
476    // Helper function to create a spectator session for testing
477    fn create_test_spectator_session() -> Option<SpectatorSession<TestConfig>> {
478        SessionBuilder::new()
479            .with_num_players(2)
480            .unwrap()
481            .start_spectator_session(test_addr(7000), DummySocket)
482    }
483
484    fn create_test_spectator_session_with_config(
485        num_players: usize,
486        buffer_size: usize,
487        max_frames_behind: usize,
488        catchup_speed: usize,
489    ) -> Option<SpectatorSession<TestConfig>> {
490        use crate::SpectatorConfig;
491        SessionBuilder::new()
492            .with_num_players(num_players)
493            .unwrap()
494            .with_spectator_config(SpectatorConfig {
495                buffer_size,
496                catchup_speed,
497                max_frames_behind,
498            })
499            .start_spectator_session(test_addr(7001), DummySocket)
500    }
501
502    // ==========================================
503    // Constructor Tests
504    // ==========================================
505
506    #[test]
507    fn spectator_session_creates_successfully() {
508        let session = create_test_spectator_session();
509        assert!(session.is_some());
510    }
511
512    #[test]
513    fn spectator_session_with_custom_config() {
514        let session = create_test_spectator_session_with_config(4, 120, 20, 3);
515        assert!(session.is_some());
516        let session = session.unwrap();
517        assert_eq!(session.num_players(), 4);
518    }
519
520    #[test]
521    fn spectator_session_single_player() {
522        let session = create_test_spectator_session_with_config(1, 60, 10, 1);
523        assert!(session.is_some());
524        let session = session.unwrap();
525        assert_eq!(session.num_players(), 1);
526    }
527
528    #[test]
529    fn spectator_session_many_players() {
530        let session = create_test_spectator_session_with_config(8, 60, 10, 1);
531        assert!(session.is_some());
532        let session = session.unwrap();
533        assert_eq!(session.num_players(), 8);
534    }
535
536    // ==========================================
537    // State and Getter Tests
538    // ==========================================
539
540    #[test]
541    fn spectator_session_initial_state_is_synchronizing() {
542        let session = create_test_spectator_session().unwrap();
543        assert_eq!(session.current_state(), SessionState::Synchronizing);
544    }
545
546    #[test]
547    fn spectator_session_initial_frame_is_null() {
548        let session = create_test_spectator_session().unwrap();
549        assert_eq!(session.current_frame(), Frame::NULL);
550    }
551
552    #[test]
553    fn spectator_session_num_players_returns_correct_count() {
554        let session2 = create_test_spectator_session_with_config(2, 60, 10, 1).unwrap();
555        assert_eq!(session2.num_players(), 2);
556
557        let session4 = create_test_spectator_session_with_config(4, 60, 10, 1).unwrap();
558        assert_eq!(session4.num_players(), 4);
559    }
560
561    #[test]
562    fn spectator_session_frames_behind_host_initially_zero() {
563        let session = create_test_spectator_session().unwrap();
564        // Both last_recv_frame and current_frame start at NULL (Frame(-1))
565        // so frames_behind_host should be 0
566        assert_eq!(session.frames_behind_host(), 0);
567    }
568
569    // ==========================================
570    // advance_frame Tests
571    // ==========================================
572
573    #[test]
574    fn spectator_session_advance_frame_returns_not_synchronized_when_not_running() {
575        let mut session = create_test_spectator_session().unwrap();
576
577        // Session starts in Synchronizing state
578        let result = session.advance_frame();
579        assert!(result.is_err());
580        assert!(matches!(result, Err(FortressError::NotSynchronized)));
581    }
582
583    // ==========================================
584    // network_stats Tests
585    // ==========================================
586
587    #[test]
588    fn spectator_session_network_stats_returns_not_synchronized_initially() {
589        let session = create_test_spectator_session().unwrap();
590
591        // Network stats should fail when not synchronized
592        let result = session.network_stats();
593        assert!(result.is_err());
594    }
595
596    // ==========================================
597    // events Tests
598    // ==========================================
599
600    #[test]
601    fn spectator_session_events_initially_empty() {
602        let mut session = create_test_spectator_session().unwrap();
603        let events: Vec<_> = session.events().collect();
604        assert!(events.is_empty());
605    }
606
607    #[test]
608    fn spectator_session_events_drains_queue() {
609        let mut session = create_test_spectator_session().unwrap();
610
611        // First call to events
612        let events1: Vec<_> = session.events().collect();
613        assert!(events1.is_empty());
614
615        // Second call should also be empty (queue was drained)
616        let events2: Vec<_> = session.events().collect();
617        assert!(events2.is_empty());
618    }
619
620    // ==========================================
621    // violation_observer Tests
622    // ==========================================
623
624    #[test]
625    fn spectator_session_violation_observer_none_by_default() {
626        let session = create_test_spectator_session().unwrap();
627        assert!(session.violation_observer().is_none());
628    }
629
630    #[test]
631    fn spectator_session_with_violation_observer() {
632        use crate::telemetry::CollectingObserver;
633
634        let observer = Arc::new(CollectingObserver::new());
635        let session: Option<SpectatorSession<TestConfig>> = SessionBuilder::new()
636            .with_num_players(2)
637            .unwrap()
638            .with_violation_observer(observer)
639            .start_spectator_session(test_addr(7002), DummySocket);
640
641        let session = session.unwrap();
642        assert!(session.violation_observer().is_some());
643    }
644
645    // ==========================================
646    // poll_remote_clients Tests
647    // ==========================================
648
649    #[test]
650    fn spectator_session_poll_remote_clients_does_not_panic() {
651        let mut session = create_test_spectator_session().unwrap();
652
653        // Polling should not panic even with no messages
654        session.poll_remote_clients();
655
656        // State should still be synchronizing (no sync messages received)
657        assert_eq!(session.current_state(), SessionState::Synchronizing);
658    }
659
660    #[test]
661    fn spectator_session_poll_remote_clients_multiple_times() {
662        let mut session = create_test_spectator_session().unwrap();
663
664        // Multiple polls should not cause issues
665        for _ in 0..10 {
666            session.poll_remote_clients();
667        }
668
669        assert_eq!(session.current_state(), SessionState::Synchronizing);
670    }
671
672    // ==========================================
673    // SpectatorConfig Tests
674    // ==========================================
675
676    #[test]
677    fn spectator_config_default_values() {
678        use crate::SpectatorConfig;
679
680        let config = SpectatorConfig::default();
681        assert_eq!(config.buffer_size, 60);
682        assert_eq!(config.catchup_speed, 1);
683        assert_eq!(config.max_frames_behind, 10);
684    }
685
686    #[test]
687    fn spectator_config_new_equals_default() {
688        use crate::SpectatorConfig;
689
690        let new_config = SpectatorConfig::new();
691        let default_config = SpectatorConfig::default();
692        assert_eq!(new_config, default_config);
693    }
694
695    #[test]
696    fn spectator_config_fast_paced_preset() {
697        use crate::SpectatorConfig;
698
699        let config = SpectatorConfig::fast_paced();
700        assert_eq!(config.buffer_size, 90);
701        assert_eq!(config.catchup_speed, 2);
702        assert_eq!(config.max_frames_behind, 15);
703    }
704
705    #[test]
706    fn spectator_config_slow_connection_preset() {
707        use crate::SpectatorConfig;
708
709        let config = SpectatorConfig::slow_connection();
710        assert_eq!(config.buffer_size, 120);
711        assert_eq!(config.catchup_speed, 1);
712        assert_eq!(config.max_frames_behind, 20);
713    }
714
715    #[test]
716    fn spectator_config_local_preset() {
717        use crate::SpectatorConfig;
718
719        let config = SpectatorConfig::local();
720        assert_eq!(config.buffer_size, 30);
721        assert_eq!(config.catchup_speed, 2);
722        assert_eq!(config.max_frames_behind, 5);
723    }
724
725    #[test]
726    fn spectator_config_equality() {
727        use crate::SpectatorConfig;
728
729        let a = SpectatorConfig {
730            buffer_size: 100,
731            catchup_speed: 2,
732            max_frames_behind: 15,
733        };
734        let b = SpectatorConfig {
735            buffer_size: 100,
736            catchup_speed: 2,
737            max_frames_behind: 15,
738        };
739        assert_eq!(a, b);
740    }
741
742    #[test]
743    fn spectator_config_inequality() {
744        use crate::SpectatorConfig;
745
746        let a = SpectatorConfig::default();
747        let b = SpectatorConfig::fast_paced();
748        assert_ne!(a, b);
749    }
750
751    #[test]
752    fn spectator_config_clone() {
753        use crate::SpectatorConfig;
754
755        let original = SpectatorConfig::fast_paced();
756        let cloned = original;
757        assert_eq!(original, cloned);
758    }
759
760    #[test]
761    fn spectator_config_debug_format() {
762        use crate::SpectatorConfig;
763
764        let config = SpectatorConfig::default();
765        let debug_str = format!("{:?}", config);
766        assert!(debug_str.contains("SpectatorConfig"));
767        assert!(debug_str.contains("buffer_size"));
768        assert!(debug_str.contains("60"));
769    }
770
771    #[test]
772    fn spectator_config_all_presets_are_distinct() {
773        use crate::SpectatorConfig;
774
775        let default = SpectatorConfig::default();
776        let fast_paced = SpectatorConfig::fast_paced();
777        let slow_connection = SpectatorConfig::slow_connection();
778        let local = SpectatorConfig::local();
779
780        // All presets should be different
781        assert_ne!(default, fast_paced);
782        assert_ne!(default, slow_connection);
783        assert_ne!(default, local);
784        assert_ne!(fast_paced, slow_connection);
785        assert_ne!(fast_paced, local);
786        assert_ne!(slow_connection, local);
787    }
788
789    // ==========================================
790    // Edge Case Tests
791    // ==========================================
792
793    #[test]
794    fn spectator_session_with_minimum_buffer_size() {
795        // Buffer size of 1 should work (edge case)
796        let session = create_test_spectator_session_with_config(2, 1, 10, 1);
797        assert!(session.is_some());
798    }
799
800    #[test]
801    fn spectator_session_with_zero_buffer_size_uses_minimum() {
802        // Buffer size of 0 should be handled (converted to 1 internally)
803        let session = create_test_spectator_session_with_config(2, 0, 10, 1);
804        assert!(session.is_some());
805    }
806
807    #[test]
808    fn spectator_session_with_large_buffer_size() {
809        let session = create_test_spectator_session_with_config(2, 1000, 10, 1);
810        assert!(session.is_some());
811    }
812
813    #[test]
814    fn spectator_session_with_high_catchup_speed() {
815        let session = create_test_spectator_session_with_config(2, 60, 10, 10);
816        assert!(session.is_some());
817    }
818
819    #[test]
820    fn spectator_session_with_zero_max_frames_behind() {
821        // Zero max_frames_behind means always in catchup mode
822        let session = create_test_spectator_session_with_config(2, 60, 0, 2);
823        assert!(session.is_some());
824    }
825
826    // ==========================================
827    // Internal State Tests
828    // ==========================================
829
830    #[test]
831    fn spectator_session_buffer_size_respects_minimum() {
832        // When buffer_size is 0, it should be clamped to 1
833        let session = create_test_spectator_session_with_config(2, 0, 10, 1).unwrap();
834        // buffer_size is private, but we can verify the session was created successfully
835        // The internal buffer_size.max(1) ensures this doesn't panic
836        assert_eq!(session.num_players(), 2);
837    }
838
839    #[test]
840    fn spectator_session_host_connect_status_initialized() {
841        // Verify that host_connect_status is initialized for all players
842        let session = create_test_spectator_session_with_config(4, 60, 10, 1).unwrap();
843        // We can't directly check host_connect_status, but we can verify
844        // the session was created with the correct number of players
845        assert_eq!(session.num_players(), 4);
846    }
847
848    #[test]
849    fn spectator_session_last_recv_frame_initially_null() {
850        let session = create_test_spectator_session().unwrap();
851        // last_recv_frame starts at NULL (Frame(-1)), which means
852        // frames_behind_host should be 0 (since current_frame is also NULL)
853        assert_eq!(session.frames_behind_host(), 0);
854    }
855
856    // ==========================================
857    // NORMAL_SPEED Constant Test
858    // ==========================================
859
860    #[test]
861    fn normal_speed_is_one() {
862        // NORMAL_SPEED constant should be 1 for smooth playback
863        assert_eq!(NORMAL_SPEED, 1);
864    }
865
866    // ==========================================
867    // Current Frame Tests
868    // ==========================================
869
870    #[test]
871    fn spectator_session_current_frame_is_null_initially() {
872        let session = create_test_spectator_session().unwrap();
873        assert!(session.current_frame().is_null());
874        assert_eq!(session.current_frame(), Frame::NULL);
875    }
876
877    // ==========================================
878    // Session State Tests
879    // ==========================================
880
881    #[test]
882    fn spectator_session_state_transitions() {
883        // Session starts in Synchronizing state
884        let session = create_test_spectator_session().unwrap();
885        assert_eq!(session.current_state(), SessionState::Synchronizing);
886
887        // We can't easily transition to Running without a real network connection,
888        // but we verify the initial state is correct
889    }
890
891    // ==========================================
892    // SpectatorConfig Builder Tests
893    // ==========================================
894
895    #[test]
896    fn spectator_config_with_zero_catchup_speed() {
897        use crate::SpectatorConfig;
898
899        // Catchup speed of 0 is technically valid (no frames advanced in catchup)
900        let config = SpectatorConfig {
901            buffer_size: 60,
902            catchup_speed: 0,
903            max_frames_behind: 10,
904        };
905        assert_eq!(config.catchup_speed, 0);
906    }
907
908    #[test]
909    fn spectator_config_extreme_values() {
910        use crate::SpectatorConfig;
911
912        // Test with extreme values
913        let config = SpectatorConfig {
914            buffer_size: usize::MAX,
915            catchup_speed: usize::MAX,
916            max_frames_behind: usize::MAX,
917        };
918        assert_eq!(config.buffer_size, usize::MAX);
919        assert_eq!(config.catchup_speed, usize::MAX);
920        assert_eq!(config.max_frames_behind, usize::MAX);
921    }
922
923    // ==========================================
924    // Multiple Poll Tests
925    // ==========================================
926
927    #[test]
928    fn spectator_session_poll_preserves_state() {
929        let mut session = create_test_spectator_session().unwrap();
930
931        // Record initial state
932        let initial_state = session.current_state();
933        let initial_frame = session.current_frame();
934
935        // Poll multiple times
936        for _ in 0..5 {
937            session.poll_remote_clients();
938        }
939
940        // State should not change without actual network events
941        assert_eq!(session.current_state(), initial_state);
942        assert_eq!(session.current_frame(), initial_frame);
943    }
944
945    #[test]
946    fn spectator_session_events_empty_after_drain() {
947        let mut session = create_test_spectator_session().unwrap();
948
949        // Drain events
950        let events: Vec<_> = session.events().collect();
951        assert!(events.is_empty());
952
953        // Poll and drain again
954        session.poll_remote_clients();
955        let events: Vec<_> = session.events().collect();
956        assert!(events.is_empty());
957    }
958
959    // ==========================================
960    // Network Stats Edge Cases
961    // ==========================================
962
963    #[test]
964    fn spectator_session_network_stats_before_sync() {
965        let session = create_test_spectator_session().unwrap();
966
967        // Should fail when not synchronized
968        let result = session.network_stats();
969        assert!(result.is_err());
970        assert!(matches!(result, Err(FortressError::NotSynchronized)));
971    }
972
973    // ==========================================
974    // Violation Observer Tests
975    // ==========================================
976
977    #[test]
978    fn spectator_session_violation_observer_is_arc() {
979        use crate::telemetry::CollectingObserver;
980
981        let observer = Arc::new(CollectingObserver::new());
982        let observer_clone = Arc::clone(&observer);
983
984        let session: Option<SpectatorSession<TestConfig>> = SessionBuilder::new()
985            .with_num_players(2)
986            .unwrap()
987            .with_violation_observer(observer)
988            .start_spectator_session(test_addr(7003), DummySocket);
989
990        let session = session.unwrap();
991
992        // Observer should be accessible
993        assert!(session.violation_observer().is_some());
994
995        // The clone should still be usable (Arc reference counting)
996        assert_eq!(observer_clone.violations().len(), 0);
997    }
998
999    #[test]
1000    fn spectator_session_without_violation_observer() {
1001        let session = create_test_spectator_session().unwrap();
1002        assert!(session.violation_observer().is_none());
1003    }
1004
1005    // ==========================================
1006    // Frames Behind Host Edge Cases
1007    // ==========================================
1008
1009    #[test]
1010    fn spectator_session_frames_behind_with_both_null() {
1011        let session = create_test_spectator_session().unwrap();
1012        // Both last_recv_frame and current_frame are NULL
1013        // NULL - NULL = 0, so frames_behind should be 0
1014        assert_eq!(session.frames_behind_host(), 0);
1015    }
1016}