1use std::collections::BTreeMap;
2use std::fmt;
3use std::sync::Arc;
4
5use crate::error::{FortressError, InternalErrorKind, InvalidRequestKind};
6use crate::frame_info::PlayerInput;
7use crate::network::messages::ConnectionStatus;
8use crate::report_violation;
9use crate::sessions::config::SaveMode;
10use crate::sync_layer::SyncLayer;
11use crate::telemetry::{ViolationKind, ViolationObserver, ViolationSeverity};
12use crate::{Config, FortressRequest, Frame, PlayerHandle};
13
14pub struct SyncTestSession<T>
18where
19 T: Config,
20{
21 num_players: usize,
22 max_prediction: usize,
23 check_distance: usize,
24 sync_layer: SyncLayer<T>,
25 dummy_connect_status: Vec<ConnectionStatus>,
26 checksum_history: BTreeMap<Frame, Option<u128>>,
27 local_inputs: BTreeMap<PlayerHandle, PlayerInput<T::Input>>,
28 violation_observer: Option<Arc<dyn ViolationObserver>>,
30}
31
32impl<T: Config> SyncTestSession<T> {
33 #[allow(dead_code)]
38 pub(crate) fn new(
39 num_players: usize,
40 max_prediction: usize,
41 check_distance: usize,
42 input_delay: usize,
43 violation_observer: Option<Arc<dyn ViolationObserver>>,
44 ) -> Self {
45 Self::with_queue_length(
46 num_players,
47 max_prediction,
48 check_distance,
49 input_delay,
50 violation_observer,
51 crate::input_queue::INPUT_QUEUE_LENGTH,
52 )
53 }
54
55 pub(crate) fn with_queue_length(
56 num_players: usize,
57 max_prediction: usize,
58 check_distance: usize,
59 input_delay: usize,
60 violation_observer: Option<Arc<dyn ViolationObserver>>,
61 queue_length: usize,
62 ) -> Self {
63 let dummy_connect_status = vec![ConnectionStatus::default(); num_players];
64
65 let mut sync_layer =
66 SyncLayer::with_queue_length(num_players, max_prediction, queue_length);
67 for i in 0..num_players {
68 if let Err(e) = sync_layer.set_frame_delay(PlayerHandle::new(i), input_delay) {
70 report_violation!(
71 ViolationSeverity::Critical,
72 ViolationKind::InternalError,
73 "Failed to set frame delay for player {} during session construction: {}",
74 i,
75 e
76 );
77 }
78 }
79
80 Self {
81 num_players,
82 max_prediction,
83 check_distance,
84 sync_layer,
85 dummy_connect_status,
86 checksum_history: BTreeMap::new(),
87 local_inputs: BTreeMap::new(),
88 violation_observer,
89 }
90 }
91
92 pub fn add_local_input(
101 &mut self,
102 player_handle: PlayerHandle,
103 input: T::Input,
104 ) -> Result<(), FortressError> {
105 if !player_handle.is_valid_player_for(self.num_players) {
106 return Err(InvalidRequestKind::InvalidLocalPlayerHandle {
107 handle: player_handle,
108 num_players: self.num_players,
109 }
110 .into());
111 }
112 let player_input = PlayerInput::<T::Input>::new(self.sync_layer.current_frame(), input);
113 self.local_inputs.insert(player_handle, player_input);
114 Ok(())
115 }
116
117 #[must_use = "FortressRequests must be processed to advance the game state"]
127 pub fn advance_frame(&mut self) -> Result<Vec<FortressRequest<T>>, FortressError> {
128 let mut requests = Vec::with_capacity(2);
131
132 let current_frame = self.sync_layer.current_frame();
134 if self.check_distance > 0 && current_frame.as_i32() > self.check_distance as i32 {
135 let oldest_frame_to_check = current_frame.as_i32() - self.check_distance as i32;
137 let mismatched_frames: Vec<_> = (oldest_frame_to_check..=current_frame.as_i32())
138 .filter(|&frame_to_check| !self.checksums_consistent(Frame::new(frame_to_check)))
139 .map(Frame::new)
140 .collect();
141
142 if !mismatched_frames.is_empty() {
143 return Err(FortressError::MismatchedChecksum {
144 current_frame,
145 mismatched_frames,
146 });
147 }
148
149 let frame_to = self.sync_layer.current_frame() - self.check_distance as i32;
151 self.adjust_gamestate(frame_to, &mut requests)?;
152 }
153
154 if self.num_players != self.local_inputs.len() {
156 return Err(InvalidRequestKind::MissingLocalInput.into());
157 }
158 for (&handle, &input) in self.local_inputs.iter() {
160 self.sync_layer.add_local_input(handle, input);
162 }
163 self.local_inputs.clear();
165
166 if self.check_distance > 0 {
169 requests.push(self.sync_layer.save_current_state());
170 }
171
172 let inputs = match self
174 .sync_layer
175 .synchronized_inputs(&self.dummy_connect_status)
176 {
177 Some(inputs) => inputs,
178 None => {
179 report_violation!(
180 ViolationSeverity::Critical,
181 ViolationKind::InternalError,
182 "Failed to get synchronized inputs for frame {}",
183 self.sync_layer.current_frame()
184 );
185 return Err(FortressError::InternalErrorStructured {
186 kind: InternalErrorKind::SynchronizedInputsFailed {
187 frame: self.sync_layer.current_frame(),
188 },
189 });
190 },
191 };
192
193 requests.push(FortressRequest::AdvanceFrame { inputs });
195 self.sync_layer.advance_frame();
196
197 let safe_frame = self.sync_layer.current_frame() - self.check_distance as i32;
200
201 self.sync_layer
202 .set_last_confirmed_frame(safe_frame, SaveMode::EveryFrame);
203
204 for con_stat in &mut self.dummy_connect_status {
206 con_stat.last_frame = self.sync_layer.current_frame();
207 }
208
209 Ok(requests)
210 }
211
212 #[must_use]
214 pub fn current_frame(&self) -> Frame {
215 self.sync_layer.current_frame()
216 }
217
218 #[must_use]
220 pub fn num_players(&self) -> usize {
221 self.num_players
222 }
223
224 #[must_use]
226 pub fn max_prediction(&self) -> usize {
227 self.max_prediction
228 }
229
230 #[must_use]
232 pub fn check_distance(&self) -> usize {
233 self.check_distance
234 }
235
236 #[must_use]
243 pub fn violation_observer(&self) -> Option<&Arc<dyn ViolationObserver>> {
244 self.violation_observer.as_ref()
245 }
246
247 fn checksums_consistent(&mut self, frame_to_check: Frame) -> bool {
249 let oldest_allowed_frame = self.sync_layer.current_frame() - self.check_distance as i32;
251 self.checksum_history
252 .retain(|&k, _| k >= oldest_allowed_frame);
253
254 match self.sync_layer.saved_state_by_frame(frame_to_check) {
255 Some(latest_cell) => match self.checksum_history.get(&latest_cell.frame()) {
256 Some(&cs) => cs == latest_cell.checksum(),
257 None => {
258 self.checksum_history
259 .insert(latest_cell.frame(), latest_cell.checksum());
260 true
261 },
262 },
263 None => true,
264 }
265 }
266
267 fn adjust_gamestate(
268 &mut self,
269 frame_to: Frame,
270 requests: &mut Vec<FortressRequest<T>>,
271 ) -> Result<(), FortressError> {
272 let start_frame = self.sync_layer.current_frame();
273 let count = start_frame - frame_to;
274
275 requests.push(self.sync_layer.load_frame(frame_to)?);
277 self.sync_layer.reset_prediction();
278 let actual_frame = self.sync_layer.current_frame();
279 if actual_frame != frame_to {
280 report_violation!(
281 ViolationSeverity::Error,
282 ViolationKind::FrameSync,
283 "current frame mismatch after load: expected={}, actual={}",
284 frame_to,
285 actual_frame
286 );
287 }
288
289 for i in 0..count {
291 let inputs = match self
292 .sync_layer
293 .synchronized_inputs(&self.dummy_connect_status)
294 {
295 Some(inputs) => inputs,
296 None => {
297 report_violation!(
298 ViolationSeverity::Critical,
299 ViolationKind::InternalError,
300 "Failed to get synchronized inputs during resimulation at frame {}",
301 self.sync_layer.current_frame()
302 );
303 return Err(FortressError::InternalErrorStructured {
304 kind: InternalErrorKind::SynchronizedInputsFailed {
305 frame: self.sync_layer.current_frame(),
306 },
307 });
308 },
309 };
310
311 if i > 0 {
313 requests.push(self.sync_layer.save_current_state());
314 }
315 self.sync_layer.advance_frame();
317
318 requests.push(FortressRequest::AdvanceFrame { inputs });
319 }
320 let final_frame = self.sync_layer.current_frame();
321 if final_frame != start_frame {
322 report_violation!(
323 ViolationSeverity::Error,
324 ViolationKind::FrameSync,
325 "current frame mismatch after resimulation: expected={}, actual={}",
326 start_frame,
327 final_frame
328 );
329 }
330 Ok(())
331 }
332}
333
334impl<T: Config> fmt::Debug for SyncTestSession<T> {
335 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336 f.debug_struct("SyncTestSession")
337 .field("num_players", &self.num_players)
338 .field("max_prediction", &self.max_prediction)
339 .field("check_distance", &self.check_distance)
340 .field("current_frame", &self.sync_layer.current_frame())
341 .finish_non_exhaustive()
342 }
343}
344
345#[cfg(test)]
346#[allow(
347 clippy::panic,
348 clippy::unwrap_used,
349 clippy::expect_used,
350 clippy::indexing_slicing
351)]
352mod tests {
353 use super::*;
354 use crate::telemetry::CollectingObserver;
355 use std::net::SocketAddr;
356
357 struct TestConfig;
359
360 impl Config for TestConfig {
361 type Input = u32;
362 type State = Vec<u8>;
363 type Address = SocketAddr;
364 }
365
366 #[test]
371 fn sync_test_session_new_creates_valid_session() {
372 let session: SyncTestSession<TestConfig> = SyncTestSession::new(2, 8, 2, 2, None);
373
374 assert_eq!(session.num_players(), 2);
375 assert_eq!(session.max_prediction(), 8);
376 assert_eq!(session.check_distance(), 2);
377 assert_eq!(session.current_frame(), Frame::new(0));
378 assert!(session.violation_observer().is_none());
379 }
380
381 #[test]
382 fn sync_test_session_with_queue_length_creates_valid_session() {
383 let session: SyncTestSession<TestConfig> =
384 SyncTestSession::with_queue_length(4, 16, 3, 1, None, 64);
385
386 assert_eq!(session.num_players(), 4);
387 assert_eq!(session.max_prediction(), 16);
388 assert_eq!(session.check_distance(), 3);
389 assert_eq!(session.current_frame(), Frame::new(0));
390 }
391
392 #[test]
393 fn sync_test_session_with_violation_observer() {
394 let observer = Arc::new(CollectingObserver::new());
395 let session: SyncTestSession<TestConfig> = SyncTestSession::new(2, 8, 2, 2, Some(observer));
396
397 assert!(session.violation_observer().is_some());
398 }
399
400 #[test]
401 fn sync_test_session_single_player() {
402 let session: SyncTestSession<TestConfig> = SyncTestSession::new(1, 8, 2, 0, None);
403
404 assert_eq!(session.num_players(), 1);
405 }
406
407 #[test]
408 fn sync_test_session_zero_check_distance() {
409 let session: SyncTestSession<TestConfig> = SyncTestSession::new(2, 8, 0, 2, None);
410
411 assert_eq!(session.check_distance(), 0);
412 }
413
414 #[test]
415 fn sync_test_session_zero_input_delay() {
416 let session: SyncTestSession<TestConfig> = SyncTestSession::new(2, 8, 2, 0, None);
417
418 assert_eq!(session.current_frame(), Frame::new(0));
420 }
421
422 #[test]
427 fn add_local_input_valid_handle_succeeds() {
428 let mut session: SyncTestSession<TestConfig> = SyncTestSession::new(2, 8, 0, 0, None);
429
430 session.add_local_input(PlayerHandle::new(0), 42).unwrap();
431 session.add_local_input(PlayerHandle::new(1), 100).unwrap();
432 }
433
434 #[test]
435 fn add_local_input_invalid_handle_fails() {
436 let mut session: SyncTestSession<TestConfig> = SyncTestSession::new(2, 8, 0, 0, None);
437
438 let result = session.add_local_input(PlayerHandle::new(2), 42);
439 assert!(result.is_err());
440
441 assert!(matches!(
442 result,
443 Err(FortressError::InvalidRequestStructured {
444 kind: InvalidRequestKind::InvalidLocalPlayerHandle { .. }
445 })
446 ));
447 }
448
449 #[test]
450 fn add_local_input_overwrites_previous_input() {
451 let mut session: SyncTestSession<TestConfig> = SyncTestSession::new(1, 8, 0, 0, None);
452
453 session
455 .add_local_input(PlayerHandle::new(0), 42)
456 .expect("should succeed");
457
458 session
460 .add_local_input(PlayerHandle::new(0), 100)
461 .expect("should succeed");
462
463 let requests = session.advance_frame().expect("should advance");
465
466 let advance_request = requests
468 .iter()
469 .find(|r| matches!(r, FortressRequest::AdvanceFrame { .. }));
470 assert!(advance_request.is_some());
471
472 if let Some(FortressRequest::AdvanceFrame { inputs }) = advance_request {
473 assert_eq!(inputs[0].0, 100); }
475 }
476
477 #[test]
482 fn advance_frame_requires_all_inputs() {
483 let mut session: SyncTestSession<TestConfig> = SyncTestSession::new(2, 8, 0, 0, None);
484
485 session
487 .add_local_input(PlayerHandle::new(0), 42)
488 .expect("should succeed");
489
490 let result = session.advance_frame();
491 assert!(result.is_err());
492
493 assert!(matches!(
494 result,
495 Err(FortressError::InvalidRequestStructured {
496 kind: InvalidRequestKind::MissingLocalInput
497 })
498 ));
499 }
500
501 #[test]
502 fn advance_frame_with_all_inputs_succeeds() {
503 let mut session: SyncTestSession<TestConfig> = SyncTestSession::new(2, 8, 0, 0, None);
504
505 session
506 .add_local_input(PlayerHandle::new(0), 42)
507 .expect("should succeed");
508 session
509 .add_local_input(PlayerHandle::new(1), 100)
510 .expect("should succeed");
511
512 let requests = session.advance_frame().unwrap();
513 assert!(requests
515 .iter()
516 .any(|r| matches!(r, FortressRequest::AdvanceFrame { .. })));
517 }
518
519 #[test]
520 fn advance_frame_increments_current_frame() {
521 let mut session: SyncTestSession<TestConfig> = SyncTestSession::new(1, 8, 0, 0, None);
522
523 assert_eq!(session.current_frame(), Frame::new(0));
524
525 session
526 .add_local_input(PlayerHandle::new(0), 42)
527 .expect("should succeed");
528 session.advance_frame().expect("should advance");
529
530 assert_eq!(session.current_frame(), Frame::new(1));
531 }
532
533 #[test]
534 fn advance_frame_clears_inputs() {
535 let mut session: SyncTestSession<TestConfig> = SyncTestSession::new(1, 8, 0, 0, None);
536
537 session
538 .add_local_input(PlayerHandle::new(0), 42)
539 .expect("should succeed");
540 session.advance_frame().expect("should advance");
541
542 let result = session.advance_frame();
544 assert!(result.is_err());
545 }
546
547 #[test]
548 fn advance_frame_with_check_distance_produces_save_request() {
549 let mut session: SyncTestSession<TestConfig> = SyncTestSession::new(1, 8, 2, 0, None);
550
551 session
552 .add_local_input(PlayerHandle::new(0), 42)
553 .expect("should succeed");
554
555 let requests = session.advance_frame().expect("should advance");
556
557 assert!(requests
559 .iter()
560 .any(|r| matches!(r, FortressRequest::SaveGameState { .. })));
561 }
562
563 #[test]
564 fn advance_frame_multiple_times() {
565 let mut session: SyncTestSession<TestConfig> = SyncTestSession::new(1, 8, 0, 0, None);
566
567 for frame in 1..=10 {
568 session
569 .add_local_input(PlayerHandle::new(0), frame as u32)
570 .expect("should succeed");
571 session.advance_frame().expect("should advance");
572 assert_eq!(session.current_frame(), Frame::new(frame));
573 }
574 }
575
576 #[test]
577 fn advance_frame_no_input_for_any_player() {
578 let mut session: SyncTestSession<TestConfig> = SyncTestSession::new(2, 8, 0, 0, None);
579
580 let result = session.advance_frame();
582 assert!(result.is_err());
583
584 assert!(matches!(
585 result,
586 Err(FortressError::InvalidRequestStructured {
587 kind: InvalidRequestKind::MissingLocalInput
588 })
589 ));
590 }
591
592 #[test]
597 fn current_frame_starts_at_zero() {
598 let session: SyncTestSession<TestConfig> = SyncTestSession::new(2, 8, 2, 2, None);
599 assert_eq!(session.current_frame(), Frame::new(0));
600 }
601
602 #[test]
603 fn num_players_returns_correct_value() {
604 for num_players in 1..=4 {
605 let session: SyncTestSession<TestConfig> =
606 SyncTestSession::new(num_players, 8, 2, 2, None);
607 assert_eq!(session.num_players(), num_players);
608 }
609 }
610
611 #[test]
612 fn max_prediction_returns_correct_value() {
613 for max_prediction in [4, 8, 16, 32] {
614 let session: SyncTestSession<TestConfig> =
615 SyncTestSession::new(2, max_prediction, 2, 2, None);
616 assert_eq!(session.max_prediction(), max_prediction);
617 }
618 }
619
620 #[test]
621 fn check_distance_returns_correct_value() {
622 for check_distance in 0..=10 {
623 let session: SyncTestSession<TestConfig> =
624 SyncTestSession::new(2, 8, check_distance, 2, None);
625 assert_eq!(session.check_distance(), check_distance);
626 }
627 }
628
629 #[test]
630 fn violation_observer_none_when_not_set() {
631 let session: SyncTestSession<TestConfig> = SyncTestSession::new(2, 8, 2, 2, None);
632 assert!(session.violation_observer().is_none());
633 }
634
635 #[test]
636 fn violation_observer_some_when_set() {
637 let observer = Arc::new(CollectingObserver::new());
638 let session: SyncTestSession<TestConfig> = SyncTestSession::new(2, 8, 2, 2, Some(observer));
639
640 let stored_observer = session.violation_observer();
641 assert!(stored_observer.is_some());
642 }
643
644 #[test]
649 fn many_players_construction() {
650 let session: SyncTestSession<TestConfig> = SyncTestSession::new(8, 16, 4, 2, None);
652
653 assert_eq!(session.num_players(), 8);
654 assert_eq!(session.max_prediction(), 16);
655 }
656
657 #[test]
658 fn large_check_distance() {
659 let session: SyncTestSession<TestConfig> = SyncTestSession::new(2, 64, 32, 2, None);
661
662 assert_eq!(session.check_distance(), 32);
663 assert_eq!(session.max_prediction(), 64);
664 }
665
666 #[test]
667 fn small_queue_length() {
668 let session: SyncTestSession<TestConfig> =
669 SyncTestSession::with_queue_length(2, 8, 2, 2, None, 16);
670
671 assert_eq!(session.num_players(), 2);
672 }
673
674 fn run_session_with_checksums(
681 session: &mut SyncTestSession<TestConfig>,
682 num_frames: usize,
683 checksum_fn: impl Fn(Frame) -> Option<u128>,
684 ) {
685 let mut game_state: Vec<u8> = Vec::new();
686
687 for frame_num in 0..num_frames {
688 for player_id in 0..session.num_players() {
690 session
691 .add_local_input(PlayerHandle::new(player_id), frame_num as u32)
692 .expect("should succeed");
693 }
694
695 let requests = session.advance_frame().expect("should advance");
697
698 for request in requests {
699 match request {
700 FortressRequest::SaveGameState { cell, frame } => {
701 let checksum = checksum_fn(frame);
702 cell.save(frame, Some(game_state.clone()), checksum);
703 },
704 FortressRequest::LoadGameState { cell, .. } => {
705 if let Some(loaded) = cell.load() {
706 game_state = loaded;
707 }
708 },
709 FortressRequest::AdvanceFrame { .. } => {
710 game_state.push(frame_num as u8);
712 },
713 }
714 }
715 }
716 }
717
718 #[test]
719 fn sync_test_with_consistent_checksums_succeeds() {
720 let mut session: SyncTestSession<TestConfig> = SyncTestSession::new(2, 8, 3, 0, None);
721
722 run_session_with_checksums(&mut session, 20, |frame| Some(frame.as_i32() as u128));
724
725 assert_eq!(session.current_frame(), Frame::new(20));
727 }
728
729 #[test]
730 fn sync_test_with_no_checksums_succeeds() {
731 let mut session: SyncTestSession<TestConfig> = SyncTestSession::new(2, 8, 3, 0, None);
732
733 run_session_with_checksums(&mut session, 15, |_| None);
735
736 assert_eq!(session.current_frame(), Frame::new(15));
737 }
738
739 #[test]
740 fn sync_test_detects_mismatched_checksum() {
741 let mut session: SyncTestSession<TestConfig> = SyncTestSession::new(1, 8, 2, 0, None);
742
743 let mut game_state: Vec<u8> = Vec::new();
744 let mut call_count = 0;
745
746 let mut detected_mismatch = false;
748 for frame_num in 0..20 {
749 session
750 .add_local_input(PlayerHandle::new(0), frame_num as u32)
751 .expect("should succeed");
752
753 match session.advance_frame() {
754 Ok(requests) => {
755 for request in requests {
756 match request {
757 FortressRequest::SaveGameState { cell, frame } => {
758 call_count += 1;
759 let checksum = if call_count > 5 {
762 Some(9999)
764 } else {
765 Some(frame.as_i32() as u128)
766 };
767 cell.save(frame, Some(game_state.clone()), checksum);
768 },
769 FortressRequest::LoadGameState { cell, .. } => {
770 if let Some(loaded) = cell.load() {
771 game_state = loaded;
772 }
773 },
774 FortressRequest::AdvanceFrame { .. } => {
775 game_state.push(frame_num as u8);
776 },
777 }
778 }
779 },
780 Err(FortressError::MismatchedChecksum { .. }) => {
781 detected_mismatch = true;
782 break;
783 },
784 Err(e) => panic!("Unexpected error: {:?}", e),
785 }
786 }
787
788 assert!(
790 detected_mismatch,
791 "Should have detected mismatched checksums"
792 );
793 }
794
795 #[test]
796 fn sync_test_zero_check_distance_skips_rollback() {
797 let mut session: SyncTestSession<TestConfig> = SyncTestSession::new(1, 8, 0, 0, None);
798
799 session
801 .add_local_input(PlayerHandle::new(0), 42)
802 .expect("should succeed");
803
804 let requests = session.advance_frame().expect("should advance");
805
806 assert!(!requests
808 .iter()
809 .any(|r| matches!(r, FortressRequest::SaveGameState { .. })));
810 assert!(!requests
811 .iter()
812 .any(|r| matches!(r, FortressRequest::LoadGameState { .. })));
813 assert!(requests
814 .iter()
815 .any(|r| matches!(r, FortressRequest::AdvanceFrame { .. })));
816 }
817
818 #[test]
819 fn sync_test_rollback_happens_after_check_distance_frames() {
820 let check_distance = 3;
821 let mut session: SyncTestSession<TestConfig> =
822 SyncTestSession::new(1, 8, check_distance, 0, None);
823
824 let mut game_state: Vec<u8> = Vec::new();
825 let mut saw_load_request = false;
826
827 for frame_num in 0..=(check_distance + 2) {
829 session
830 .add_local_input(PlayerHandle::new(0), frame_num as u32)
831 .expect("should succeed");
832
833 let requests = session.advance_frame().expect("should advance");
834
835 for request in requests {
836 match request {
837 FortressRequest::SaveGameState { cell, frame } => {
838 cell.save(
839 frame,
840 Some(game_state.clone()),
841 Some(frame.as_i32() as u128),
842 );
843 },
844 FortressRequest::LoadGameState { cell, .. } => {
845 saw_load_request = true;
846 if let Some(loaded) = cell.load() {
847 game_state = loaded;
848 }
849 },
850 FortressRequest::AdvanceFrame { .. } => {
851 game_state.push(frame_num as u8);
852 },
853 }
854 }
855 }
856
857 assert!(
859 saw_load_request,
860 "Should have seen LoadGameState after passing check_distance"
861 );
862 }
863
864 #[test]
865 fn sync_test_many_players_with_checksums() {
866 let mut session: SyncTestSession<TestConfig> = SyncTestSession::new(4, 8, 2, 0, None);
867
868 run_session_with_checksums(&mut session, 10, |frame| Some(frame.as_i32() as u128));
870
871 assert_eq!(session.current_frame(), Frame::new(10));
872 assert_eq!(session.num_players(), 4);
873 }
874
875 #[test]
876 fn sync_test_large_check_distance() {
877 let check_distance = 10;
878 let mut session: SyncTestSession<TestConfig> =
879 SyncTestSession::new(1, 32, check_distance, 0, None);
880
881 run_session_with_checksums(&mut session, 30, |frame| Some(frame.as_i32() as u128));
882
883 assert_eq!(session.current_frame(), Frame::new(30));
884 assert_eq!(session.check_distance(), check_distance);
885 }
886
887 #[test]
892 fn requests_contain_advance_frame_with_inputs() {
893 let mut session: SyncTestSession<TestConfig> = SyncTestSession::new(2, 8, 0, 0, None);
894
895 session
896 .add_local_input(PlayerHandle::new(0), 111)
897 .expect("should succeed");
898 session
899 .add_local_input(PlayerHandle::new(1), 222)
900 .expect("should succeed");
901
902 let requests = session.advance_frame().expect("should advance");
903
904 let advance_request = requests
905 .iter()
906 .find(|r| matches!(r, FortressRequest::AdvanceFrame { .. }));
907 assert!(
908 advance_request.is_some(),
909 "Should have AdvanceFrame request"
910 );
911
912 if let Some(FortressRequest::AdvanceFrame { inputs }) = advance_request {
913 assert_eq!(inputs.len(), 2);
914 assert_eq!(inputs[0].0, 111);
915 assert_eq!(inputs[1].0, 222);
916 }
917 }
918
919 #[test]
920 fn requests_order_save_before_advance() {
921 let mut session: SyncTestSession<TestConfig> = SyncTestSession::new(1, 8, 2, 0, None);
922
923 session
924 .add_local_input(PlayerHandle::new(0), 42)
925 .expect("should succeed");
926
927 let requests = session.advance_frame().expect("should advance");
928
929 let save_pos = requests
931 .iter()
932 .position(|r| matches!(r, FortressRequest::SaveGameState { .. }));
933 let advance_pos = requests
934 .iter()
935 .position(|r| matches!(r, FortressRequest::AdvanceFrame { .. }));
936
937 assert!(
939 save_pos.is_some(),
940 "Should have SaveGameState with check_distance > 0"
941 );
942 assert!(advance_pos.is_some(), "Should have AdvanceFrame");
943
944 assert!(
947 save_pos < advance_pos,
948 "SaveGameState should come before the final AdvanceFrame"
949 );
950 }
951
952 #[test]
957 fn sync_test_via_session_builder() {
958 use crate::SessionBuilder;
959
960 let session: SyncTestSession<TestConfig> = SessionBuilder::new()
961 .with_num_players(2)
962 .unwrap()
963 .with_max_prediction_window(8)
964 .with_check_distance(3)
965 .start_synctest_session()
966 .expect("should create session");
967
968 assert_eq!(session.num_players(), 2);
969 assert_eq!(session.max_prediction(), 8);
970 assert_eq!(session.check_distance(), 3);
971 }
972
973 #[test]
974 fn sync_test_builder_with_input_delay() {
975 use crate::SessionBuilder;
976
977 let session: SyncTestSession<TestConfig> = SessionBuilder::new()
978 .with_num_players(2)
979 .unwrap()
980 .with_input_delay(3)
981 .unwrap()
982 .with_check_distance(2)
983 .start_synctest_session()
984 .expect("should create session");
985
986 assert_eq!(session.num_players(), 2);
987 }
988
989 #[test]
990 fn sync_test_builder_with_observer() {
991 use crate::SessionBuilder;
992
993 let observer = Arc::new(CollectingObserver::new());
994 let session: SyncTestSession<TestConfig> = SessionBuilder::new()
995 .with_num_players(2)
996 .unwrap()
997 .with_violation_observer(observer)
998 .start_synctest_session()
999 .expect("should create session");
1000
1001 assert!(session.violation_observer().is_some());
1002 }
1003
1004 #[test]
1009 fn checksum_history_is_pruned_over_time() {
1010 let check_distance = 2;
1013 let mut session: SyncTestSession<TestConfig> =
1014 SyncTestSession::new(1, 8, check_distance, 0, None);
1015
1016 let mut game_state: Vec<u8> = Vec::new();
1017
1018 for frame_num in 0..50 {
1020 session
1021 .add_local_input(PlayerHandle::new(0), frame_num as u32)
1022 .expect("should succeed");
1023
1024 let requests = session.advance_frame().expect("should advance");
1025
1026 for request in requests {
1027 match request {
1028 FortressRequest::SaveGameState { cell, frame } => {
1029 cell.save(
1030 frame,
1031 Some(game_state.clone()),
1032 Some(frame.as_i32() as u128),
1033 );
1034 },
1035 FortressRequest::LoadGameState { cell, .. } => {
1036 if let Some(loaded) = cell.load() {
1037 game_state = loaded;
1038 }
1039 },
1040 FortressRequest::AdvanceFrame { .. } => {
1041 game_state.push(frame_num as u8);
1042 },
1043 }
1044 }
1045 }
1046
1047 assert_eq!(session.current_frame(), Frame::new(50));
1050 }
1051
1052 #[test]
1057 fn advance_frame_returns_confirmed_input_status() {
1058 let mut session: SyncTestSession<TestConfig> = SyncTestSession::new(1, 8, 0, 0, None);
1059
1060 session
1061 .add_local_input(PlayerHandle::new(0), 42)
1062 .expect("should succeed");
1063
1064 let requests = session.advance_frame().expect("should advance");
1065
1066 if let Some(FortressRequest::AdvanceFrame { inputs }) = requests
1067 .iter()
1068 .find(|r| matches!(r, FortressRequest::AdvanceFrame { .. }))
1069 {
1070 assert_eq!(inputs[0].1, crate::InputStatus::Confirmed);
1072 } else {
1073 panic!("Should have AdvanceFrame request");
1074 }
1075 }
1076}