fortress_rollback/lib.rs
1//! # Fortress Rollback (formerly GGRS)
2//!
3//! <p align="center">
4//! <img src="https://raw.githubusercontent.com/wallstop/fortress-rollback/main/docs/assets/logo-banner.svg" alt="Fortress Rollback" width="400">
5//! </p>
6//!
7//! Fortress Rollback is a fortified, verified reimagination of the GGPO network SDK written in 100% safe Rust.
8//! The callback-style API from the original library has been replaced with a simple request-driven control flow.
9//! Instead of registering callback functions, Fortress Rollback (previously GGRS) returns a list of requests for the user to fulfill.
10
11#![forbid(unsafe_code)] // let us try
12#![deny(warnings)] // Treat all warnings as errors (matches CI behavior)
13#![deny(missing_docs)]
14#![deny(rustdoc::broken_intra_doc_links)]
15#![deny(rustdoc::private_intra_doc_links)]
16#![deny(rustdoc::invalid_codeblock_attributes)]
17#![warn(rustdoc::invalid_html_tags)]
18#![warn(rustdoc::bare_urls)]
19//#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
20use std::{fmt::Debug, hash::Hash};
21
22pub use error::{
23 DeltaDecodeReason, FortressError, IndexOutOfBounds, InternalErrorKind, InvalidFrameReason,
24 InvalidRequestKind, RleDecodeReason, SerializationErrorKind, SocketErrorKind,
25};
26
27/// A specialized `Result` type for Fortress Rollback operations.
28///
29/// This type alias provides a convenient way to write function signatures
30/// that return [`FortressError`] as the error type. It supports an optional
31/// second type parameter to override the error type if needed.
32///
33/// # Naming
34///
35/// This type is named `FortressResult` rather than `Result` to avoid
36/// shadowing `std::result::Result` when using glob imports like
37/// `use fortress_rollback::*;` or `use fortress_rollback::prelude::*;`.
38/// This prevents subtle semver hazards where downstream code might
39/// unexpectedly use this alias instead of the standard library's `Result`.
40///
41/// # Examples
42///
43/// Using the default error type:
44///
45/// ```
46/// use fortress_rollback::{FortressResult, FortressError};
47///
48/// fn process_frame() -> FortressResult<()> {
49/// // Returns Result<(), FortressError>
50/// Ok(())
51/// }
52/// ```
53///
54/// Overriding the error type:
55///
56/// ```
57/// use fortress_rollback::FortressResult;
58///
59/// fn custom_operation() -> FortressResult<String, std::io::Error> {
60/// // Returns Result<String, std::io::Error>
61/// Ok("success".to_string())
62/// }
63/// ```
64///
65/// You can also alias it locally if you prefer a shorter name:
66///
67/// ```
68/// use fortress_rollback::FortressResult as Result;
69///
70/// fn my_function() -> Result<()> {
71/// Ok(())
72/// }
73/// ```
74pub type FortressResult<T, E = FortressError> = std::result::Result<T, E>;
75
76pub use network::chaos_socket::{ChaosConfig, ChaosConfigBuilder, ChaosSocket, ChaosStats};
77pub use network::messages::Message;
78pub use network::network_stats::NetworkStats;
79pub use network::udp_socket::UdpNonBlockingSocket;
80use serde::{de::DeserializeOwned, Serialize};
81pub use sessions::builder::SessionBuilder;
82pub use sessions::config::{
83 InputQueueConfig, ProtocolConfig, SaveMode, SpectatorConfig, SyncConfig,
84};
85pub use sessions::p2p_session::P2PSession;
86pub use sessions::p2p_spectator_session::SpectatorSession;
87pub use sessions::player_registry::PlayerRegistry;
88pub use sessions::sync_health::SyncHealth;
89pub use sessions::sync_test_session::SyncTestSession;
90// Re-export smallvec for users who need to work with InputVec directly
91pub use smallvec::SmallVec;
92pub use sync_layer::{GameStateAccessor, GameStateCell};
93pub use time_sync::TimeSyncConfig;
94
95// Re-export prediction strategies
96pub use crate::input_queue::{BlankPrediction, PredictionStrategy, RepeatLastConfirmed};
97
98// Re-export checksum utilities for easy access
99pub use checksum::{compute_checksum, compute_checksum_fletcher16, fletcher16, hash_bytes_fnv1a};
100
101/// Tokio async runtime integration for Fortress Rollback.
102///
103/// This module provides [`TokioUdpSocket`], an adapter that wraps a Tokio async UDP socket
104/// and implements the [`NonBlockingSocket`] trait for use with Fortress Rollback sessions
105/// in async Tokio applications.
106///
107/// # Feature Flag
108///
109/// This module requires the `tokio` feature flag:
110///
111/// ```toml
112/// [dependencies]
113/// fortress-rollback = { version = "0.4", features = ["tokio"] }
114/// ```
115///
116/// # Example
117///
118/// ```ignore
119/// use fortress_rollback::tokio_socket::TokioUdpSocket;
120/// use fortress_rollback::{SessionBuilder, PlayerType, PlayerHandle};
121///
122/// #[tokio::main]
123/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
124/// // Create and bind a Tokio UDP socket adapter
125/// let socket = TokioUdpSocket::bind_to_port(7000).await?;
126///
127/// // Use with SessionBuilder
128/// let session = SessionBuilder::<MyConfig>::new()
129/// .with_num_players(2)?
130/// .add_player(PlayerType::Local, PlayerHandle::new(0))?
131/// .add_player(PlayerType::Remote(remote_addr), PlayerHandle::new(1))?
132/// .start_p2p_session(socket)?;
133///
134/// // Game loop...
135/// Ok(())
136/// }
137/// ```
138///
139/// [`TokioUdpSocket`]: crate::tokio_socket::TokioUdpSocket
140/// [`NonBlockingSocket`]: crate::NonBlockingSocket
141#[cfg(feature = "tokio")]
142pub mod tokio_socket {
143 pub use crate::network::tokio_socket::TokioUdpSocket;
144}
145
146/// State checksum utilities for rollback networking.
147///
148/// Provides deterministic checksum computation for game states, essential for
149/// desync detection in peer-to-peer rollback networking.
150///
151/// # Quick Start
152///
153/// ```
154/// use fortress_rollback::checksum::{compute_checksum, ChecksumError};
155/// use serde::Serialize;
156///
157/// #[derive(Serialize)]
158/// struct GameState { frame: u32, x: f32, y: f32 }
159///
160/// let state = GameState { frame: 100, x: 1.0, y: 2.0 };
161/// let checksum = compute_checksum(&state)?;
162/// # Ok::<(), ChecksumError>(())
163/// ```
164///
165/// See module documentation for detailed usage and performance considerations.
166pub mod checksum;
167
168/// Convenient re-exports for common usage.
169///
170/// This module provides a "prelude" that re-exports the most commonly used types
171/// from Fortress Rollback, allowing you to import them all at once with
172/// `use fortress_rollback::prelude::*;`
173///
174/// See the [`prelude`] module documentation for the full list of included types.
175pub mod prelude;
176
177// Internal modules - made pub for re-export in __internal, but doc(hidden) for API cleanliness
178#[doc(hidden)]
179pub mod error;
180#[doc(hidden)]
181pub mod frame_info;
182pub mod hash;
183#[doc(hidden)]
184pub mod input_queue;
185/// Internal run-length encoding module for network compression.
186///
187/// Provides RLE encoding/decoding that replaces the `bitfield-rle` crate dependency.
188/// See the module documentation for usage details.
189pub mod rle;
190/// Internal random number generator module based on PCG32.
191///
192/// Provides a minimal, high-quality PRNG that replaces the `rand` crate dependency.
193/// See the module documentation for usage details.
194pub mod rng;
195#[doc(hidden)]
196pub mod sync;
197#[doc(hidden)]
198pub mod sync_layer;
199pub mod telemetry;
200/// Shared test configuration for property-based testing.
201///
202/// This module provides centralized configuration for proptest, including
203/// Miri-aware case count reduction for faster testing under the interpreter.
204#[cfg(test)]
205pub(crate) mod test_config;
206#[doc(hidden)]
207pub mod time_sync;
208#[doc(hidden)]
209pub mod sessions {
210 #[doc(hidden)]
211 pub mod builder;
212 /// Configuration types for session behavior.
213 #[doc(hidden)]
214 pub mod config;
215 #[doc(hidden)]
216 pub mod p2p_session;
217 #[doc(hidden)]
218 pub mod p2p_spectator_session;
219 #[doc(hidden)]
220 pub mod player_registry;
221 #[doc(hidden)]
222 pub mod sync_health;
223 #[doc(hidden)]
224 pub mod sync_test_session;
225}
226#[doc(hidden)]
227pub mod network {
228 pub mod chaos_socket;
229 /// Binary codec for network message serialization.
230 ///
231 /// Provides centralized, zero-allocation-where-possible encoding and decoding
232 /// of network messages using bincode.
233 pub mod codec;
234 #[doc(hidden)]
235 pub mod compression;
236 #[doc(hidden)]
237 pub mod messages;
238 #[doc(hidden)]
239 pub mod network_stats;
240 #[doc(hidden)]
241 pub mod protocol;
242 #[cfg(feature = "tokio")]
243 pub mod tokio_socket;
244 #[doc(hidden)]
245 pub mod udp_socket;
246}
247
248/// Internal module exposing implementation details for testing, fuzzing, and formal verification.
249///
250/// # ⚠️ WARNING: No Stability Guarantees
251///
252/// **This module is NOT part of the public API.** Everything here is:
253/// - Subject to change without notice
254/// - Not covered by semver compatibility guarantees
255/// - Intended ONLY for:
256/// - Fuzzing (cargo-fuzz, libFuzzer, AFL)
257/// - Property-based testing (proptest)
258/// - Formal verification (Kani, Z3)
259/// - Integration testing in the same workspace
260///
261/// **DO NOT** depend on anything in this module for production code.
262/// **DO NOT** import these types in your game/application code.
263///
264/// # Rationale
265///
266/// Rollback networking has complex invariants that benefit from direct testing
267/// of internal components:
268/// - **InputQueue**: Circular buffer with prediction, frame delay, rollback semantics
269/// - **SyncLayer**: Frame synchronization, state management, rollback coordination
270/// - **TimeSync**: Time synchronization calculations and averaging
271/// - **Compression**: Delta encoding for network efficiency
272/// - **Protocol**: State machine for peer connections
273///
274/// By exposing these internals (with clear warnings), we enable:
275/// 1. Higher fuzz coverage (direct component testing vs. through session APIs)
276/// 2. Better fault isolation (pinpoint which component failed)
277/// 3. Direct invariant testing (test component contracts directly)
278/// 4. Same code paths for testing and production (no `#[cfg(test)]` divergence)
279///
280/// # Example: Fuzz Target
281///
282/// ```ignore
283/// use fortress_rollback::__internal::{InputQueue, PlayerInput};
284/// use fortress_rollback::Frame;
285///
286/// // Direct fuzzing of InputQueue (not possible without this module)
287/// fuzz_target!(|ops: Vec<QueueOp>| {
288/// let mut queue = InputQueue::<TestConfig>::with_queue_length(0, 32);
289/// for op in ops {
290/// match op {
291/// QueueOp::Add(frame, input) => queue.add_input(PlayerInput::new(frame, input)),
292/// QueueOp::Get(frame) => queue.input(frame),
293/// // ...
294/// }
295/// }
296/// });
297/// ```
298#[doc(hidden)]
299pub mod __internal {
300
301 // Core types
302 pub use crate::frame_info::{GameState, PlayerInput};
303 pub use crate::input_queue::{InputQueue, INPUT_QUEUE_LENGTH, MAX_FRAME_DELAY};
304 pub use crate::sync_layer::{GameStateCell, SavedStates, SyncLayer};
305 pub use crate::time_sync::TimeSync;
306
307 // Network internals
308 pub use crate::network::compression::{decode, delta_decode, delta_encode, encode};
309 pub use crate::network::messages::ConnectionStatus;
310 pub use crate::network::protocol::{Event, ProtocolState, UdpProtocol};
311
312 // RLE compression (internal implementation)
313 pub use crate::rle::{decode as rle_decode, encode as rle_encode};
314
315 // Session internals
316 pub use crate::sessions::player_registry::PlayerRegistry;
317}
318
319// #############
320// # CONSTANTS #
321// #############
322
323/// Internally, -1 represents no frame / invalid frame.
324///
325/// # Formal Specification Alignment
326/// - **TLA+**: `NULL_FRAME = 999` in `specs/tla/*.cfg` (uses 999 to stay in Nat domain)
327/// - **Z3**: `NULL_FRAME = -1` in `tests/test_z3_verification.rs`
328/// - **formal-spec.md**: `NULL_FRAME = -1`, with `VALID_FRAME(f) ↔ f ≥ 0`
329pub const NULL_FRAME: i32 = -1;
330
331/// A frame is a single step of game execution.
332///
333/// Frames are the fundamental unit of time in rollback networking. Each frame
334/// represents one discrete step of game simulation. Frame numbers start at 0
335/// and increment sequentially.
336///
337/// The special value [`NULL_FRAME`] (-1) represents "no frame" or "uninitialized".
338///
339/// # Formal Specification Alignment
340/// - **TLA+**: `Frame == {NULL_FRAME} ∪ (0..MAX_FRAME)` in `specs/tla/Rollback.tla`
341/// - **Z3**: Frame arithmetic proofs in `tests/test_z3_verification.rs`
342/// - **formal-spec.md**: Core type definition with operations `frame_add`, `frame_sub`, `frame_valid`
343/// - **Kani**: `kani_frame_*` proofs verify overflow safety and arithmetic correctness
344///
345/// # Type Safety
346///
347/// `Frame` is a newtype wrapper around `i32` that provides:
348/// - Clear semantic meaning (frames vs arbitrary integers)
349/// - Helper methods like [`is_null()`](Frame::is_null) and [`is_valid()`](Frame::is_valid)
350/// - Arithmetic operations for frame calculations
351/// - Compile-time prevention of accidentally mixing frames with other integers
352///
353/// # Examples
354///
355/// ```
356/// use fortress_rollback::{Frame, NULL_FRAME};
357///
358/// // Creating frames
359/// let frame = Frame::new(0);
360/// let null_frame = Frame::NULL;
361///
362/// // Checking validity
363/// assert!(frame.is_valid());
364/// assert!(null_frame.is_null());
365///
366/// // Frame arithmetic
367/// let next_frame = frame + 1;
368/// assert_eq!(next_frame.as_i32(), 1);
369///
370/// // Comparison
371/// assert!(next_frame > frame);
372/// ```
373#[derive(
374 Debug,
375 Copy,
376 Clone,
377 PartialEq,
378 Eq,
379 PartialOrd,
380 Ord,
381 Hash,
382 Default,
383 serde::Serialize,
384 serde::Deserialize,
385)]
386pub struct Frame(i32);
387
388impl Frame {
389 /// The null frame constant, representing "no frame" or "uninitialized".
390 ///
391 /// This is equivalent to [`NULL_FRAME`] (-1).
392 pub const NULL: Self = Self(NULL_FRAME);
393
394 /// Creates a new `Frame` from an `i32` value.
395 ///
396 /// Note: This does not validate the frame number. Use [`Frame::is_valid()`]
397 /// to check if the frame represents a valid (non-negative) frame number.
398 #[inline]
399 #[must_use]
400 pub const fn new(frame: i32) -> Self {
401 Self(frame)
402 }
403
404 /// Returns the underlying `i32` value.
405 #[inline]
406 #[must_use]
407 pub const fn as_i32(self) -> i32 {
408 self.0
409 }
410
411 /// Returns `true` if this frame is the null frame (equivalent to [`NULL_FRAME`]).
412 ///
413 /// # Examples
414 ///
415 /// ```
416 /// use fortress_rollback::Frame;
417 ///
418 /// assert!(Frame::NULL.is_null());
419 /// assert!(!Frame::new(0).is_null());
420 /// ```
421 #[inline]
422 #[must_use]
423 pub const fn is_null(self) -> bool {
424 self.0 == NULL_FRAME
425 }
426
427 /// Returns `true` if this frame is valid (non-negative).
428 ///
429 /// # Examples
430 ///
431 /// ```
432 /// use fortress_rollback::Frame;
433 ///
434 /// assert!(Frame::new(0).is_valid());
435 /// assert!(Frame::new(100).is_valid());
436 /// assert!(!Frame::NULL.is_valid());
437 /// assert!(!Frame::new(-5).is_valid());
438 /// ```
439 #[inline]
440 #[must_use]
441 pub const fn is_valid(self) -> bool {
442 self.0 >= 0
443 }
444
445 /// Returns `Some(self)` if the frame is valid, or `None` if it's null or negative.
446 ///
447 /// This is useful for handling the null/valid frame pattern with Option.
448 #[inline]
449 #[must_use]
450 pub const fn to_option(self) -> Option<Self> {
451 if self.is_valid() {
452 Some(self)
453 } else {
454 None
455 }
456 }
457
458 /// Creates a Frame from an Option, using NULL for None.
459 #[inline]
460 #[must_use]
461 pub const fn from_option(opt: Option<Self>) -> Self {
462 match opt {
463 Some(f) => f,
464 None => Self::NULL,
465 }
466 }
467
468 // === Checked Arithmetic Methods ===
469 //
470 // Design Philosophy: Graceful error handling over panics.
471 //
472 // These methods are the PREFERRED way to perform Frame arithmetic in production code.
473 // They allow the library to handle edge cases gracefully rather than panicking.
474 //
475 // Guidelines:
476 // - Use `checked_*` when you need to detect and handle overflow explicitly
477 // - Use `saturating_*` when clamping to bounds is acceptable behavior
478 // - Use `abs_diff` when calculating frame distances (order-independent)
479 // - Avoid raw `+` and `-` operators except in tests or where overflow is impossible
480 //
481 // Note: While `overflow-checks = true` in release catches overflow as panics,
482 // the goal is zero panics in production - use these methods proactively.
483
484 /// Adds a value to this frame, returning `None` if overflow occurs.
485 ///
486 /// This is the preferred method for frame arithmetic when overflow must be handled.
487 ///
488 /// # Examples
489 ///
490 /// ```
491 /// use fortress_rollback::Frame;
492 ///
493 /// let frame = Frame::new(100);
494 /// assert_eq!(frame.checked_add(50), Some(Frame::new(150)));
495 /// assert_eq!(Frame::new(i32::MAX).checked_add(1), None);
496 /// ```
497 #[inline]
498 #[must_use]
499 pub const fn checked_add(self, rhs: i32) -> Option<Self> {
500 match self.0.checked_add(rhs) {
501 Some(result) => Some(Self(result)),
502 None => None,
503 }
504 }
505
506 /// Subtracts a value from this frame, returning `None` if overflow occurs.
507 ///
508 /// This is the preferred method for frame arithmetic when overflow must be handled.
509 ///
510 /// # Examples
511 ///
512 /// ```
513 /// use fortress_rollback::Frame;
514 ///
515 /// let frame = Frame::new(100);
516 /// assert_eq!(frame.checked_sub(50), Some(Frame::new(50)));
517 /// assert_eq!(Frame::new(i32::MIN).checked_sub(1), None);
518 /// ```
519 #[inline]
520 #[must_use]
521 pub const fn checked_sub(self, rhs: i32) -> Option<Self> {
522 match self.0.checked_sub(rhs) {
523 Some(result) => Some(Self(result)),
524 None => None,
525 }
526 }
527
528 /// Adds a value to this frame, saturating at the numeric bounds.
529 ///
530 /// Use this when clamping to bounds is acceptable (e.g., frame counters that
531 /// should never go negative or exceed maximum).
532 ///
533 /// # Examples
534 ///
535 /// ```
536 /// use fortress_rollback::Frame;
537 ///
538 /// let frame = Frame::new(100);
539 /// assert_eq!(frame.saturating_add(50), Frame::new(150));
540 /// assert_eq!(Frame::new(i32::MAX).saturating_add(1), Frame::new(i32::MAX));
541 /// ```
542 #[inline]
543 #[must_use]
544 pub const fn saturating_add(self, rhs: i32) -> Self {
545 Self(self.0.saturating_add(rhs))
546 }
547
548 /// Subtracts a value from this frame, saturating at the numeric bounds.
549 ///
550 /// Use this when clamping to bounds is acceptable (e.g., ensuring frame
551 /// never goes below zero or `i32::MIN`).
552 ///
553 /// # Examples
554 ///
555 /// ```
556 /// use fortress_rollback::Frame;
557 ///
558 /// let frame = Frame::new(100);
559 /// assert_eq!(frame.saturating_sub(50), Frame::new(50));
560 /// assert_eq!(Frame::new(i32::MIN).saturating_sub(1), Frame::new(i32::MIN));
561 /// ```
562 #[inline]
563 #[must_use]
564 pub const fn saturating_sub(self, rhs: i32) -> Self {
565 Self(self.0.saturating_sub(rhs))
566 }
567
568 /// Returns the absolute difference between two frames.
569 ///
570 /// This is useful for calculating frame distances without worrying about
571 /// the order of operands.
572 ///
573 /// # Examples
574 ///
575 /// ```
576 /// use fortress_rollback::Frame;
577 ///
578 /// let a = Frame::new(100);
579 /// let b = Frame::new(150);
580 /// assert_eq!(a.abs_diff(b), 50);
581 /// assert_eq!(b.abs_diff(a), 50);
582 /// ```
583 #[inline]
584 #[must_use]
585 pub const fn abs_diff(self, other: Self) -> u32 {
586 self.0.abs_diff(other.0)
587 }
588
589 // === Ergonomic Conversion Methods ===
590
591 /// Returns the frame as a `usize`, or `None` if the frame is negative.
592 ///
593 /// This is useful for indexing into arrays or vectors where a valid
594 /// (non-negative) frame is required.
595 ///
596 /// # Examples
597 ///
598 /// ```
599 /// use fortress_rollback::Frame;
600 ///
601 /// assert_eq!(Frame::new(42).as_usize(), Some(42));
602 /// assert_eq!(Frame::new(0).as_usize(), Some(0));
603 /// assert_eq!(Frame::NULL.as_usize(), None);
604 /// assert_eq!(Frame::new(-5).as_usize(), None);
605 /// ```
606 #[inline]
607 #[must_use]
608 pub const fn as_usize(self) -> Option<usize> {
609 if self.0 >= 0 {
610 Some(self.0 as usize)
611 } else {
612 None
613 }
614 }
615
616 /// Returns the frame as a `usize`, or a `FortressError` if negative.
617 ///
618 /// This is the Result-returning version of [`as_usize`](Self::as_usize),
619 /// useful when you want to use the `?` operator for error propagation.
620 ///
621 /// # Errors
622 ///
623 /// Returns [`FortressError::InvalidFrameStructured`] with reason
624 /// [`InvalidFrameReason::MustBeNonNegative`] if the frame is negative.
625 ///
626 /// # Examples
627 ///
628 /// ```
629 /// use fortress_rollback::{Frame, FortressError, InvalidFrameReason};
630 ///
631 /// // Successful conversion
632 /// let value = Frame::new(42).try_as_usize()?;
633 /// assert_eq!(value, 42);
634 ///
635 /// // Error case - negative frame
636 /// let result = Frame::NULL.try_as_usize();
637 /// assert!(matches!(
638 /// result,
639 /// Err(FortressError::InvalidFrameStructured {
640 /// frame,
641 /// reason: InvalidFrameReason::MustBeNonNegative,
642 /// }) if frame == Frame::NULL
643 /// ));
644 /// # Ok::<(), FortressError>(())
645 /// ```
646 ///
647 /// [`InvalidFrameReason::MustBeNonNegative`]: crate::InvalidFrameReason::MustBeNonNegative
648 #[inline]
649 #[track_caller]
650 pub fn try_as_usize(self) -> Result<usize, FortressError> {
651 if self.0 >= 0 {
652 Ok(self.0 as usize)
653 } else {
654 Err(FortressError::InvalidFrameStructured {
655 frame: self,
656 reason: InvalidFrameReason::MustBeNonNegative,
657 })
658 }
659 }
660
661 /// Calculates the buffer index for this frame using modular arithmetic.
662 ///
663 /// This is a common pattern for ring buffer indexing where you need to map
664 /// a frame number to a buffer slot. Returns `None` if the frame is negative
665 /// or if `buffer_size` is zero.
666 ///
667 /// # Examples
668 ///
669 /// ```
670 /// use fortress_rollback::Frame;
671 ///
672 /// // Frame 7 in a buffer of size 4 -> index 3
673 /// assert_eq!(Frame::new(7).buffer_index(4), Some(3));
674 ///
675 /// // Frame 0 in a buffer of size 4 -> index 0
676 /// assert_eq!(Frame::new(0).buffer_index(4), Some(0));
677 ///
678 /// // Negative frame returns None
679 /// assert_eq!(Frame::NULL.buffer_index(4), None);
680 ///
681 /// // Zero buffer size returns None
682 /// assert_eq!(Frame::new(5).buffer_index(0), None);
683 /// ```
684 #[inline]
685 #[must_use]
686 pub const fn buffer_index(self, buffer_size: usize) -> Option<usize> {
687 if self.0 >= 0 && buffer_size > 0 {
688 Some(self.0 as usize % buffer_size)
689 } else {
690 None
691 }
692 }
693
694 /// Calculates the buffer index for this frame, returning an error for invalid frames.
695 ///
696 /// This is the Result-returning version of [`buffer_index()`][Self::buffer_index].
697 ///
698 /// # Errors
699 ///
700 /// Returns [`FortressError::InvalidFrameStructured`] if the frame is negative.
701 /// Returns [`FortressError::InvalidRequestStructured`] with [`InvalidRequestKind::ZeroBufferSize`]
702 /// if `buffer_size` is zero.
703 ///
704 /// # Examples
705 ///
706 /// ```
707 /// use fortress_rollback::{Frame, FortressError, InvalidRequestKind};
708 ///
709 /// // Valid frame and buffer size
710 /// let index = Frame::new(7).try_buffer_index(4)?;
711 /// assert_eq!(index, 3);
712 ///
713 /// // Negative frame returns error
714 /// assert!(Frame::NULL.try_buffer_index(4).is_err());
715 ///
716 /// // Zero buffer size returns error
717 /// let result = Frame::new(5).try_buffer_index(0);
718 /// assert!(matches!(
719 /// result,
720 /// Err(FortressError::InvalidRequestStructured {
721 /// kind: InvalidRequestKind::ZeroBufferSize
722 /// })
723 /// ));
724 /// # Ok::<(), FortressError>(())
725 /// ```
726 ///
727 /// [`InvalidRequestKind::ZeroBufferSize`]: crate::InvalidRequestKind::ZeroBufferSize
728 #[inline]
729 #[track_caller]
730 pub fn try_buffer_index(self, buffer_size: usize) -> Result<usize, FortressError> {
731 if buffer_size == 0 {
732 return Err(FortressError::InvalidRequestStructured {
733 kind: InvalidRequestKind::ZeroBufferSize,
734 });
735 }
736 self.try_as_usize().map(|u| u % buffer_size)
737 }
738
739 // === Result-Returning Arithmetic ===
740
741 /// Adds a value to this frame, returning an error if overflow occurs.
742 ///
743 /// This is the Result-returning version of [`checked_add`](Self::checked_add),
744 /// useful when you want to use the `?` operator for error propagation.
745 ///
746 /// # Errors
747 ///
748 /// Returns [`FortressError::FrameArithmeticOverflow`] if the addition would overflow.
749 ///
750 /// # Examples
751 ///
752 /// ```
753 /// use fortress_rollback::{Frame, FortressError};
754 ///
755 /// let frame = Frame::new(100);
756 /// let result = frame.try_add(50)?;
757 /// assert_eq!(result, Frame::new(150));
758 ///
759 /// // Overflow returns error
760 /// let overflow_result = Frame::new(i32::MAX).try_add(1);
761 /// assert!(matches!(overflow_result, Err(FortressError::FrameArithmeticOverflow { .. })));
762 /// # Ok::<(), FortressError>(())
763 /// ```
764 #[inline]
765 #[track_caller]
766 pub fn try_add(self, rhs: i32) -> Result<Self, FortressError> {
767 self.checked_add(rhs)
768 .ok_or(FortressError::FrameArithmeticOverflow {
769 frame: self,
770 operand: rhs,
771 operation: "add",
772 })
773 }
774
775 /// Subtracts a value from this frame, returning an error if overflow occurs.
776 ///
777 /// This is the Result-returning version of [`checked_sub`](Self::checked_sub),
778 /// useful when you want to use the `?` operator for error propagation.
779 ///
780 /// # Errors
781 ///
782 /// Returns [`FortressError::FrameArithmeticOverflow`] if the subtraction would overflow.
783 ///
784 /// # Examples
785 ///
786 /// ```
787 /// use fortress_rollback::{Frame, FortressError};
788 ///
789 /// let frame = Frame::new(100);
790 /// let result = frame.try_sub(50)?;
791 /// assert_eq!(result, Frame::new(50));
792 ///
793 /// // Overflow returns error
794 /// let overflow_result = Frame::new(i32::MIN).try_sub(1);
795 /// assert!(matches!(overflow_result, Err(FortressError::FrameArithmeticOverflow { .. })));
796 /// # Ok::<(), FortressError>(())
797 /// ```
798 #[inline]
799 #[track_caller]
800 pub fn try_sub(self, rhs: i32) -> Result<Self, FortressError> {
801 self.checked_sub(rhs)
802 .ok_or(FortressError::FrameArithmeticOverflow {
803 frame: self,
804 operand: rhs,
805 operation: "sub",
806 })
807 }
808
809 // === Convenience Increment/Decrement Methods ===
810
811 /// Returns the next frame, or an error if overflow would occur.
812 ///
813 /// This is equivalent to `try_add(1)`.
814 ///
815 /// # Errors
816 ///
817 /// Returns [`FortressError::FrameArithmeticOverflow`] if the frame is `i32::MAX`.
818 ///
819 /// # Examples
820 ///
821 /// ```
822 /// use fortress_rollback::{Frame, FortressError};
823 ///
824 /// let next_frame = Frame::new(5).next()?;
825 /// assert_eq!(next_frame, Frame::new(6));
826 ///
827 /// // MAX returns error
828 /// assert!(Frame::new(i32::MAX).next().is_err());
829 /// # Ok::<(), FortressError>(())
830 /// ```
831 #[inline]
832 #[track_caller]
833 pub fn next(self) -> Result<Self, FortressError> {
834 self.try_add(1)
835 }
836
837 /// Returns the previous frame, or an error if overflow would occur.
838 ///
839 /// This is equivalent to `try_sub(1)`.
840 ///
841 /// # Errors
842 ///
843 /// Returns [`FortressError::FrameArithmeticOverflow`] if the frame is `i32::MIN`.
844 ///
845 /// # Examples
846 ///
847 /// ```
848 /// use fortress_rollback::{Frame, FortressError};
849 ///
850 /// let prev_frame = Frame::new(5).prev()?;
851 /// assert_eq!(prev_frame, Frame::new(4));
852 ///
853 /// // MIN returns error
854 /// assert!(Frame::new(i32::MIN).prev().is_err());
855 /// # Ok::<(), FortressError>(())
856 /// ```
857 #[inline]
858 #[track_caller]
859 pub fn prev(self) -> Result<Self, FortressError> {
860 self.try_sub(1)
861 }
862
863 /// Returns the next frame, saturating at `i32::MAX`.
864 ///
865 /// This is equivalent to `saturating_add(1)`.
866 ///
867 /// # Examples
868 ///
869 /// ```
870 /// use fortress_rollback::Frame;
871 ///
872 /// assert_eq!(Frame::new(5).saturating_next(), Frame::new(6));
873 /// assert_eq!(Frame::new(i32::MAX).saturating_next(), Frame::new(i32::MAX));
874 /// ```
875 #[inline]
876 #[must_use]
877 pub const fn saturating_next(self) -> Self {
878 self.saturating_add(1)
879 }
880
881 /// Returns the previous frame, saturating at `i32::MIN`.
882 ///
883 /// This is equivalent to `saturating_sub(1)`.
884 ///
885 /// # Examples
886 ///
887 /// ```
888 /// use fortress_rollback::Frame;
889 ///
890 /// assert_eq!(Frame::new(5).saturating_prev(), Frame::new(4));
891 /// assert_eq!(Frame::new(i32::MIN).saturating_prev(), Frame::new(i32::MIN));
892 /// ```
893 #[inline]
894 #[must_use]
895 pub const fn saturating_prev(self) -> Self {
896 self.saturating_sub(1)
897 }
898
899 // === Safe usize Construction ===
900
901 /// Creates a `Frame` from a `usize`, returning `None` if it exceeds `i32::MAX`.
902 ///
903 /// This is useful for converting array indices or sizes to frames safely.
904 ///
905 /// # Examples
906 ///
907 /// ```
908 /// use fortress_rollback::Frame;
909 ///
910 /// assert_eq!(Frame::from_usize(42), Some(Frame::new(42)));
911 /// assert_eq!(Frame::from_usize(0), Some(Frame::new(0)));
912 ///
913 /// // Values exceeding i32::MAX return None
914 /// let too_large = (i32::MAX as usize) + 1;
915 /// assert_eq!(Frame::from_usize(too_large), None);
916 /// ```
917 #[inline]
918 #[must_use]
919 pub const fn from_usize(value: usize) -> Option<Self> {
920 if value <= i32::MAX as usize {
921 Some(Self(value as i32))
922 } else {
923 None
924 }
925 }
926
927 /// Creates a `Frame` from a `usize`, returning an error if it exceeds `i32::MAX`.
928 ///
929 /// This is the Result-returning version of [`from_usize`](Self::from_usize),
930 /// useful when you want to use the `?` operator for error propagation.
931 ///
932 /// # Errors
933 ///
934 /// Returns [`FortressError::FrameValueTooLarge`] if the value exceeds `i32::MAX`.
935 ///
936 /// # Examples
937 ///
938 /// ```
939 /// use fortress_rollback::{Frame, FortressError};
940 ///
941 /// let frame = Frame::try_from_usize(42)?;
942 /// assert_eq!(frame, Frame::new(42));
943 ///
944 /// // Values exceeding i32::MAX return error
945 /// let too_large = (i32::MAX as usize) + 1;
946 /// let result = Frame::try_from_usize(too_large);
947 /// assert!(matches!(result, Err(FortressError::FrameValueTooLarge { .. })));
948 /// # Ok::<(), FortressError>(())
949 /// ```
950 #[inline]
951 #[track_caller]
952 pub fn try_from_usize(value: usize) -> Result<Self, FortressError> {
953 Self::from_usize(value).ok_or(FortressError::FrameValueTooLarge { value })
954 }
955
956 // === Distance and Range Methods ===
957
958 /// Returns the signed distance from `self` to `other` (`other - self`).
959 ///
960 /// Returns `None` if the subtraction would overflow.
961 ///
962 /// # Examples
963 ///
964 /// ```
965 /// use fortress_rollback::Frame;
966 ///
967 /// let a = Frame::new(100);
968 /// let b = Frame::new(150);
969 ///
970 /// assert_eq!(a.distance_to(b), Some(50));
971 /// assert_eq!(b.distance_to(a), Some(-50));
972 /// assert_eq!(a.distance_to(a), Some(0));
973 ///
974 /// // Overflow returns None
975 /// assert_eq!(Frame::new(i32::MIN).distance_to(Frame::new(i32::MAX)), None);
976 /// ```
977 #[inline]
978 #[must_use]
979 pub const fn distance_to(self, other: Self) -> Option<i32> {
980 other.0.checked_sub(self.0)
981 }
982
983 /// Returns `true` if `self` is within `window` frames of `reference`.
984 ///
985 /// This checks if the absolute difference between `self` and `reference`
986 /// is less than or equal to `window`.
987 ///
988 /// # Examples
989 ///
990 /// ```
991 /// use fortress_rollback::Frame;
992 ///
993 /// let reference = Frame::new(100);
994 ///
995 /// // Within window
996 /// assert!(Frame::new(98).is_within(5, reference)); // diff = 2
997 /// assert!(Frame::new(105).is_within(5, reference)); // diff = 5
998 ///
999 /// // At boundary
1000 /// assert!(Frame::new(95).is_within(5, reference)); // diff = 5 (exact)
1001 ///
1002 /// // Outside window
1003 /// assert!(!Frame::new(94).is_within(5, reference)); // diff = 6
1004 /// assert!(!Frame::new(106).is_within(5, reference)); // diff = 6
1005 /// ```
1006 #[inline]
1007 #[must_use]
1008 pub const fn is_within(self, window: u32, reference: Self) -> bool {
1009 self.abs_diff(reference) <= window
1010 }
1011}
1012
1013impl std::fmt::Display for Frame {
1014 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1015 if self.is_null() {
1016 write!(f, "NULL_FRAME")
1017 } else {
1018 write!(f, "{}", self.0)
1019 }
1020 }
1021}
1022
1023// Arithmetic operations
1024
1025impl std::ops::Add<i32> for Frame {
1026 type Output = Self;
1027
1028 #[inline]
1029 fn add(self, rhs: i32) -> Self::Output {
1030 Self(self.0 + rhs)
1031 }
1032}
1033
1034impl std::ops::Add<Self> for Frame {
1035 type Output = Self;
1036
1037 #[inline]
1038 fn add(self, rhs: Self) -> Self::Output {
1039 Self(self.0 + rhs.0)
1040 }
1041}
1042
1043impl std::ops::AddAssign<i32> for Frame {
1044 #[inline]
1045 fn add_assign(&mut self, rhs: i32) {
1046 self.0 += rhs;
1047 }
1048}
1049
1050impl std::ops::Sub<i32> for Frame {
1051 type Output = Self;
1052
1053 #[inline]
1054 fn sub(self, rhs: i32) -> Self::Output {
1055 Self(self.0 - rhs)
1056 }
1057}
1058
1059impl std::ops::Sub<Self> for Frame {
1060 type Output = i32;
1061
1062 #[inline]
1063 fn sub(self, rhs: Self) -> Self::Output {
1064 self.0 - rhs.0
1065 }
1066}
1067
1068impl std::ops::SubAssign<i32> for Frame {
1069 #[inline]
1070 fn sub_assign(&mut self, rhs: i32) {
1071 self.0 -= rhs;
1072 }
1073}
1074
1075impl std::ops::Rem<i32> for Frame {
1076 type Output = i32;
1077
1078 #[inline]
1079 fn rem(self, rhs: i32) -> Self::Output {
1080 self.0 % rhs
1081 }
1082}
1083
1084// Conversion traits for backwards compatibility
1085
1086impl From<i32> for Frame {
1087 #[inline]
1088 fn from(value: i32) -> Self {
1089 Self(value)
1090 }
1091}
1092
1093impl From<Frame> for i32 {
1094 #[inline]
1095 fn from(frame: Frame) -> Self {
1096 frame.0
1097 }
1098}
1099
1100/// Converts a `usize` to a `Frame`.
1101///
1102/// # ⚠️ Discouraged
1103///
1104/// **Soft-deprecated**: This conversion silently truncates values larger
1105/// than `i32::MAX`. For safe conversion with overflow detection, use
1106/// [`Frame::from_usize()`] or [`Frame::try_from_usize()`] instead.
1107///
1108/// This impl cannot use `#[deprecated]` because Rust doesn't support that attribute
1109/// on trait impl blocks — no compiler warning will be emitted. Consider using the
1110/// safer alternatives listed above.
1111impl From<usize> for Frame {
1112 #[inline]
1113 fn from(value: usize) -> Self {
1114 Self(value as i32)
1115 }
1116}
1117
1118// Comparison with i32 for convenience
1119
1120impl PartialEq<i32> for Frame {
1121 #[inline]
1122 fn eq(&self, other: &i32) -> bool {
1123 self.0 == *other
1124 }
1125}
1126
1127impl PartialOrd<i32> for Frame {
1128 #[inline]
1129 fn partial_cmp(&self, other: &i32) -> Option<std::cmp::Ordering> {
1130 self.0.partial_cmp(other)
1131 }
1132}
1133
1134/// A unique identifier for a player or spectator in a session.
1135///
1136/// Player handles are the primary way to reference participants in a Fortress Rollback
1137/// session. Each player or spectator is assigned a unique handle when added to the session.
1138///
1139/// # Handle Ranges
1140///
1141/// - **Players**: Handles `0` through `num_players - 1` are reserved for active players
1142/// - **Spectators**: Handles `num_players` and above are used for spectators
1143///
1144/// # Type Safety
1145///
1146/// `PlayerHandle` is a newtype wrapper around `usize` that provides:
1147/// - Clear semantic meaning (player identifiers vs arbitrary integers)
1148/// - Helper methods like [`is_spectator_for()`](PlayerHandle::is_spectator_for)
1149/// - Compile-time prevention of accidentally mixing handles with other integers
1150///
1151/// # Examples
1152///
1153/// ```
1154/// use fortress_rollback::PlayerHandle;
1155///
1156/// // Creating handles
1157/// let player = PlayerHandle::new(0);
1158/// let spectator = PlayerHandle::new(2); // In a 2-player game
1159///
1160/// // Checking if a handle is for a spectator
1161/// assert!(!player.is_spectator_for(2));
1162/// assert!(spectator.is_spectator_for(2));
1163///
1164/// // Getting the raw value
1165/// assert_eq!(player.as_usize(), 0);
1166/// ```
1167#[derive(
1168 Debug,
1169 Copy,
1170 Clone,
1171 PartialEq,
1172 Eq,
1173 PartialOrd,
1174 Ord,
1175 Hash,
1176 Default,
1177 serde::Serialize,
1178 serde::Deserialize,
1179)]
1180pub struct PlayerHandle(usize);
1181
1182impl PlayerHandle {
1183 /// Creates a new `PlayerHandle` from a `usize` value.
1184 ///
1185 /// Note: This does not validate the handle against a specific session.
1186 /// Use [`is_valid_player_for()`](Self::is_valid_player_for) or
1187 /// [`is_spectator_for()`](Self::is_spectator_for) to check validity.
1188 #[inline]
1189 #[must_use]
1190 pub const fn new(handle: usize) -> Self {
1191 Self(handle)
1192 }
1193
1194 /// Returns the underlying `usize` value.
1195 #[inline]
1196 #[must_use]
1197 pub const fn as_usize(self) -> usize {
1198 self.0
1199 }
1200
1201 /// Returns `true` if this handle refers to a valid player (not spectator)
1202 /// for a session with the given number of players.
1203 ///
1204 /// # Examples
1205 ///
1206 /// ```
1207 /// use fortress_rollback::PlayerHandle;
1208 ///
1209 /// let handle = PlayerHandle::new(1);
1210 /// assert!(handle.is_valid_player_for(2)); // Valid for 2-player session
1211 /// assert!(!handle.is_valid_player_for(1)); // Invalid for 1-player session
1212 /// ```
1213 #[inline]
1214 #[must_use]
1215 pub const fn is_valid_player_for(self, num_players: usize) -> bool {
1216 self.0 < num_players
1217 }
1218
1219 /// Returns `true` if this handle refers to a spectator
1220 /// for a session with the given number of players.
1221 ///
1222 /// # Examples
1223 ///
1224 /// ```
1225 /// use fortress_rollback::PlayerHandle;
1226 ///
1227 /// let handle = PlayerHandle::new(2);
1228 /// assert!(handle.is_spectator_for(2)); // Spectator in 2-player session
1229 /// assert!(!handle.is_spectator_for(3)); // Player in 3-player session
1230 /// ```
1231 #[inline]
1232 #[must_use]
1233 pub const fn is_spectator_for(self, num_players: usize) -> bool {
1234 self.0 >= num_players
1235 }
1236}
1237
1238impl std::fmt::Display for PlayerHandle {
1239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1240 write!(f, "PlayerHandle({})", self.0)
1241 }
1242}
1243
1244// Conversion traits for backwards compatibility
1245
1246impl From<usize> for PlayerHandle {
1247 #[inline]
1248 fn from(value: usize) -> Self {
1249 Self(value)
1250 }
1251}
1252
1253impl From<PlayerHandle> for usize {
1254 #[inline]
1255 fn from(handle: PlayerHandle) -> Self {
1256 handle.0
1257 }
1258}
1259
1260// #############
1261// # ENUMS #
1262// #############
1263
1264/// Desync detection by comparing checksums between peers.
1265///
1266/// Defaults to [`DesyncDetection::On`] with an interval of 60 (once per second at 60hz).
1267/// This provides reasonable detection frequency while being bandwidth-friendly.
1268/// For faster detection, you can decrease the interval; for bandwidth-constrained
1269/// scenarios, you can increase the interval or disable detection entirely.
1270#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1271pub enum DesyncDetection {
1272 /// Desync detection is turned on with a specified interval rate given by the user.
1273 ///
1274 /// The interval controls how often checksums are compared. An interval of 1 means
1275 /// every frame, 10 means every 10th frame (6 times per second at 60hz), etc.
1276 On {
1277 /// Interval rate for checksum comparison. At 60hz, an interval of 1 means
1278 /// checksums are compared every frame, 10 means 6 times per second, etc.
1279 interval: u32,
1280 },
1281 /// Desync detection is turned off.
1282 ///
1283 /// **Warning:** Disabling desync detection means state divergence between peers
1284 /// will go undetected, potentially causing confusing gameplay bugs.
1285 Off,
1286}
1287
1288impl Default for DesyncDetection {
1289 /// Returns [`DesyncDetection::On`] with `interval: 60` (once per second at 60hz).
1290 fn default() -> Self {
1291 Self::On { interval: 60 }
1292 }
1293}
1294
1295impl std::fmt::Display for DesyncDetection {
1296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1297 match self {
1298 Self::On { interval } => write!(f, "On(interval={})", interval),
1299 Self::Off => write!(f, "Off"),
1300 }
1301 }
1302}
1303
1304/// Defines the three types of players that Fortress Rollback considers:
1305/// - local players, who play on the local device,
1306/// - remote players, who play on other devices and
1307/// - spectators, who are remote players that do not contribute to the game input.
1308///
1309/// Both [`PlayerType::Remote`] and [`PlayerType::Spectator`] have a socket address associated with them.
1310#[derive(Debug, Default, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
1311pub enum PlayerType<A>
1312where
1313 A: Clone + PartialEq + Eq + PartialOrd + Ord + Hash,
1314{
1315 /// This player plays on the local device.
1316 #[default]
1317 Local,
1318 /// This player plays on a remote device identified by the socket address.
1319 Remote(A),
1320 /// This player spectates on a remote device identified by the socket address. They do not contribute to the game input.
1321 Spectator(A),
1322}
1323
1324impl<A> std::fmt::Display for PlayerType<A>
1325where
1326 A: Clone + PartialEq + Eq + PartialOrd + Ord + std::hash::Hash + std::fmt::Display,
1327{
1328 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1329 match self {
1330 Self::Local => write!(f, "Local"),
1331 Self::Remote(addr) => write!(f, "Remote({})", addr),
1332 Self::Spectator(addr) => write!(f, "Spectator({})", addr),
1333 }
1334 }
1335}
1336
1337/// A session is always in one of these states. You can query the current state of a session via [`current_state`].
1338///
1339/// [`current_state`]: P2PSession#method.current_state
1340#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1341pub enum SessionState {
1342 /// When synchronizing, the session attempts to establish a connection to the remote clients.
1343 Synchronizing,
1344 /// When running, the session has synchronized and is ready to take and transmit player input.
1345 Running,
1346}
1347
1348impl std::fmt::Display for SessionState {
1349 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1350 match self {
1351 Self::Synchronizing => write!(f, "Synchronizing"),
1352 Self::Running => write!(f, "Running"),
1353 }
1354 }
1355}
1356
1357/// [`InputStatus`] will always be given together with player inputs when requested to advance the frame.
1358#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1359pub enum InputStatus {
1360 /// The input of this player for this frame is an actual received input.
1361 Confirmed,
1362 /// The input of this player for this frame is predicted.
1363 Predicted,
1364 /// The player has disconnected at or prior to this frame, so this input is a dummy.
1365 Disconnected,
1366}
1367
1368impl std::fmt::Display for InputStatus {
1369 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1370 match self {
1371 Self::Confirmed => write!(f, "Confirmed"),
1372 Self::Predicted => write!(f, "Predicted"),
1373 Self::Disconnected => write!(f, "Disconnected"),
1374 }
1375 }
1376}
1377
1378/// Stack-allocated vector type for player inputs.
1379///
1380/// This type uses [`SmallVec`] to avoid heap allocations for the common case of
1381/// 2-4 players. Games with more than 4 players will spill to the heap automatically.
1382///
1383/// # Performance
1384///
1385/// For games with 1-4 players, input vectors are stack-allocated, avoiding the
1386/// overhead of heap allocation and deallocation on every frame. This provides
1387/// measurable performance improvements in the hot path of `advance_frame()`.
1388///
1389/// # Usage
1390///
1391/// `InputVec` is used in [`FortressRequest::AdvanceFrame`] and can be iterated
1392/// like a regular slice:
1393///
1394/// ```ignore
1395/// let FortressRequest::AdvanceFrame { inputs } = request else { return };
1396/// for (input, status) in inputs.iter() {
1397/// // Process each player's input
1398/// }
1399/// ```
1400///
1401/// # Migration from `Vec`
1402///
1403/// `InputVec` implements `Deref<Target = [(T::Input, InputStatus)]>`, so most code
1404/// using `.iter()`, `.len()`, indexing, or other slice methods will work unchanged.
1405/// If you need a `Vec`, use `.to_vec()`.
1406pub type InputVec<I> = SmallVec<[(I, InputStatus); 4]>;
1407
1408/// Notifications that you can receive from the session. Handling them is up to the user.
1409///
1410/// # Handling Events
1411///
1412/// Events inform you about session state changes. Match on all variants to handle each case:
1413///
1414/// ```ignore
1415/// match event {
1416/// FortressEvent::Synchronized { addr } => { /* handle */ }
1417/// FortressEvent::Disconnected { addr } => { /* handle */ }
1418/// // ... handle all other variants
1419/// }
1420/// ```
1421#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1422pub enum FortressEvent<T>
1423where
1424 T: Config,
1425{
1426 /// The session made progress in synchronizing. After `total` roundtrips, the session are synchronized.
1427 Synchronizing {
1428 /// The address of the endpoint.
1429 addr: T::Address,
1430 /// Total number of required successful synchronization steps.
1431 total: u32,
1432 /// Current number of successful synchronization steps.
1433 count: u32,
1434 /// Total sync requests sent (includes retries due to packet loss).
1435 /// Higher values indicate network issues during synchronization.
1436 total_requests_sent: u32,
1437 /// Milliseconds elapsed since synchronization started.
1438 /// Useful for detecting slow sync due to high latency or packet loss.
1439 elapsed_ms: u128,
1440 },
1441 /// The session is now synchronized with the remote client.
1442 Synchronized {
1443 /// The address of the endpoint.
1444 addr: T::Address,
1445 },
1446 /// The remote client has disconnected.
1447 Disconnected {
1448 /// The address of the endpoint.
1449 addr: T::Address,
1450 },
1451 /// The session has not received packets from the remote client for some time and will disconnect the remote in `disconnect_timeout` ms.
1452 NetworkInterrupted {
1453 /// The address of the endpoint.
1454 addr: T::Address,
1455 /// The client will be disconnected in this amount of ms.
1456 disconnect_timeout: u128,
1457 },
1458 /// Sent only after a [`FortressEvent::NetworkInterrupted`] event, if communication with that player has resumed.
1459 NetworkResumed {
1460 /// The address of the endpoint.
1461 addr: T::Address,
1462 },
1463 /// Sent out if Fortress Rollback recommends skipping a few frames to let clients catch up. If you receive this, consider waiting `skip_frames` number of frames.
1464 WaitRecommendation {
1465 /// Amount of frames recommended to be skipped in order to let other clients catch up.
1466 skip_frames: u32,
1467 },
1468 /// Sent whenever Fortress Rollback locally detected a discrepancy between local and remote checksums
1469 DesyncDetected {
1470 /// Frame of the checksums
1471 frame: Frame,
1472 /// local checksum for the given frame
1473 local_checksum: u128,
1474 /// remote checksum for the given frame
1475 remote_checksum: u128,
1476 /// remote address of the endpoint.
1477 addr: T::Address,
1478 },
1479 /// Synchronization has timed out. This is only emitted if a sync timeout was configured
1480 /// via [`SyncConfig`]. The session will continue trying to sync, but the user may choose
1481 /// to abort and disconnect.
1482 SyncTimeout {
1483 /// The address of the endpoint that timed out.
1484 addr: T::Address,
1485 /// Milliseconds elapsed since synchronization started.
1486 elapsed_ms: u128,
1487 },
1488}
1489
1490impl<T: Config> std::fmt::Display for FortressEvent<T>
1491where
1492 T::Address: std::fmt::Display,
1493{
1494 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1495 match self {
1496 Self::Synchronizing {
1497 addr,
1498 total,
1499 count,
1500 total_requests_sent,
1501 elapsed_ms,
1502 } => write!(
1503 f,
1504 "Synchronizing({}/{}, addr={}, requests_sent={}, elapsed={}ms)",
1505 count, total, addr, total_requests_sent, elapsed_ms
1506 ),
1507 Self::Synchronized { addr } => write!(f, "Synchronized(addr={})", addr),
1508 Self::Disconnected { addr } => write!(f, "Disconnected(addr={})", addr),
1509 Self::NetworkInterrupted {
1510 addr,
1511 disconnect_timeout,
1512 } => write!(
1513 f,
1514 "NetworkInterrupted(addr={}, timeout={}ms)",
1515 addr, disconnect_timeout
1516 ),
1517 Self::NetworkResumed { addr } => write!(f, "NetworkResumed(addr={})", addr),
1518 Self::WaitRecommendation { skip_frames } => {
1519 write!(f, "WaitRecommendation(skip_frames={})", skip_frames)
1520 },
1521 Self::DesyncDetected {
1522 frame,
1523 local_checksum,
1524 remote_checksum,
1525 addr,
1526 } => write!(
1527 f,
1528 "DesyncDetected(frame={}, local={:#x}, remote={:#x}, addr={})",
1529 frame.as_i32(),
1530 local_checksum,
1531 remote_checksum,
1532 addr
1533 ),
1534 Self::SyncTimeout { addr, elapsed_ms } => {
1535 write!(f, "SyncTimeout(addr={}, elapsed={}ms)", addr, elapsed_ms)
1536 },
1537 }
1538 }
1539}
1540
1541/// Requests that you can receive from the session. Handling them is mandatory.
1542///
1543/// # ⚠️ CRITICAL: Request Ordering
1544///
1545/// **Requests MUST be fulfilled in the exact order they are returned.** The session
1546/// returns requests in a specific sequence that ensures correct simulation:
1547///
1548/// ```text
1549/// ┌──────────────────────────────────────────────────────────────┐
1550/// │ Request Flow │
1551/// ├──────────────────────────────────────────────────────────────┤
1552/// │ 1. SaveGameState ─► Save current state before advancing │
1553/// │ ↓ │
1554/// │ 2. LoadGameState ─► (During rollback) Load earlier state │
1555/// │ ↓ │
1556/// │ 3. AdvanceFrame ─► Apply inputs and advance simulation │
1557/// └──────────────────────────────────────────────────────────────┘
1558/// ```
1559///
1560/// # Why Order Matters
1561///
1562/// - **`SaveGameState` before `AdvanceFrame`**: Ensures the state can be rolled
1563/// back if a misprediction is detected later.
1564/// - **`LoadGameState` resets simulation**: When rollback occurs, loading
1565/// restores an earlier known-correct state.
1566/// - **`AdvanceFrame` uses loaded state**: After a load, advance applies
1567/// corrected inputs to the restored state.
1568///
1569/// # Consequences of Wrong Ordering
1570///
1571/// Processing requests out of order will cause:
1572/// - **Desyncs**: Wrong state saved/loaded, causing peers to diverge
1573/// - **Incorrect simulation**: Inputs applied to wrong state
1574/// - **Assertion failures**: Internal invariants violated
1575///
1576/// # Example
1577///
1578/// ```ignore
1579/// let requests = session.advance_frame()?;
1580/// // Process in order - DO NOT reorder!
1581/// for request in requests {
1582/// match request {
1583/// FortressRequest::SaveGameState { cell, frame } => {
1584/// let checksum = compute_checksum(&game_state);
1585/// cell.save(frame, Some(game_state.clone()), Some(checksum));
1586/// }
1587/// FortressRequest::LoadGameState { cell, frame } => {
1588/// if let Some(state) = cell.load() {
1589/// game_state = state;
1590/// }
1591/// }
1592/// FortressRequest::AdvanceFrame { inputs } => {
1593/// game_state.update(&inputs);
1594/// }
1595/// }
1596/// }
1597/// ```
1598#[derive(Debug, Clone)]
1599pub enum FortressRequest<T>
1600where
1601 T: Config,
1602{
1603 /// You should save the current gamestate in the `cell` provided to you. The given `frame` is a sanity check: The gamestate you save should be from that frame.
1604 SaveGameState {
1605 /// Use `cell.save(...)` to save your state.
1606 cell: GameStateCell<T::State>,
1607 /// The given `frame` is a sanity check: The gamestate you save should be from that frame.
1608 frame: Frame,
1609 },
1610 /// You should load the gamestate in the `cell` provided to you. The given `frame` is a sanity check: The gamestate you load should be from that frame.
1611 LoadGameState {
1612 /// Use `cell.load()` to load your state.
1613 cell: GameStateCell<T::State>,
1614 /// The given `frame` is a sanity check: The gamestate you load is from that frame.
1615 frame: Frame,
1616 },
1617 /// You should advance the gamestate with the `inputs` provided to you.
1618 /// Disconnected players are indicated by having [`NULL_FRAME`] instead of the correct current frame in their input.
1619 AdvanceFrame {
1620 /// Contains inputs and input status for each player.
1621 ///
1622 /// This uses [`InputVec`] (a [`SmallVec`]) instead of [`Vec`] for better performance.
1623 /// For 1-4 players, inputs are stack-allocated (no heap allocation).
1624 /// The collection implements `Deref<Target = [T]>`, so `.iter()` and indexing work normally.
1625 inputs: InputVec<T::Input>,
1626 },
1627}
1628
1629impl<T: Config> std::fmt::Display for FortressRequest<T> {
1630 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1631 match self {
1632 Self::SaveGameState { frame, .. } => {
1633 write!(f, "SaveGameState(frame={})", frame.as_i32())
1634 },
1635 Self::LoadGameState { frame, .. } => {
1636 write!(f, "LoadGameState(frame={})", frame.as_i32())
1637 },
1638 Self::AdvanceFrame { inputs } => {
1639 write!(f, "AdvanceFrame(inputs={})", inputs.len())
1640 },
1641 }
1642 }
1643}
1644
1645/// Macro to simplify handling [`FortressRequest`] variants in a game loop.
1646///
1647/// This macro eliminates the boilerplate of matching on request variants, providing
1648/// a concise way to handle save, load, and advance operations.
1649///
1650/// # Usage
1651///
1652/// ```
1653/// # use fortress_rollback::{Config, Frame, FortressRequest, GameStateCell, InputVec, handle_requests};
1654/// # use serde::{Deserialize, Serialize};
1655/// # use std::net::SocketAddr;
1656/// #
1657/// # #[derive(Copy, Clone, PartialEq, Default, Serialize, Deserialize)]
1658/// # struct MyInput(u8);
1659/// #
1660/// # #[derive(Clone, Default)]
1661/// # struct MyState { frame: i32, data: u64 }
1662/// #
1663/// # struct MyConfig;
1664/// # impl Config for MyConfig {
1665/// # type Input = MyInput;
1666/// # type State = MyState;
1667/// # type Address = SocketAddr;
1668/// # }
1669/// #
1670/// # fn compute_checksum(_: &MyState) -> u128 { 0 }
1671/// #
1672/// # fn example(mut state: MyState, requests: Vec<FortressRequest<MyConfig>>) {
1673/// handle_requests!(
1674/// requests,
1675/// save: |cell: GameStateCell<MyState>, frame: Frame| {
1676/// let checksum = compute_checksum(&state);
1677/// cell.save(frame, Some(state.clone()), Some(checksum));
1678/// },
1679/// load: |cell: GameStateCell<MyState>, _frame: Frame| {
1680/// // LoadGameState is only requested for previously saved frames.
1681/// // Handle missing state appropriately for your application.
1682/// if let Some(loaded) = cell.load() {
1683/// state = loaded;
1684/// }
1685/// },
1686/// advance: |inputs: InputVec<MyInput>| {
1687/// state.frame += 1;
1688/// // Apply inputs...
1689/// }
1690/// );
1691/// # }
1692/// ```
1693///
1694/// # Parameters
1695///
1696/// - `requests`: An iterable of [`FortressRequest<T>`] (usually `Vec<FortressRequest<T>>`)
1697/// - `save`: Closure taking `(cell: GameStateCell<State>, frame: Frame)` — called for [`FortressRequest::SaveGameState`]
1698/// - `load`: Closure taking `(cell: GameStateCell<State>, frame: Frame)` — called for [`FortressRequest::LoadGameState`]
1699/// - `advance`: Closure taking `(inputs: InputVec<Input>)` — called for [`FortressRequest::AdvanceFrame`]
1700///
1701/// # Order Preservation
1702///
1703/// Requests are processed in iteration order, which matches the order returned by
1704/// [`P2PSession::advance_frame`]. This order is critical for correctness — do not
1705/// sort, filter, or reorder the requests.
1706///
1707/// # Lockstep Mode
1708///
1709/// In lockstep mode (prediction window = 0), you will never receive `SaveGameState`
1710/// or `LoadGameState` requests. You can provide empty closures:
1711///
1712/// ```
1713/// # use fortress_rollback::{Config, Frame, FortressRequest, GameStateCell, InputVec, handle_requests};
1714/// # use serde::{Deserialize, Serialize};
1715/// # use std::net::SocketAddr;
1716/// #
1717/// # #[derive(Copy, Clone, PartialEq, Default, Serialize, Deserialize)]
1718/// # struct MyInput(u8);
1719/// # #[derive(Clone, Default)]
1720/// # struct MyState { frame: i32 }
1721/// # struct MyConfig;
1722/// # impl Config for MyConfig {
1723/// # type Input = MyInput;
1724/// # type State = MyState;
1725/// # type Address = SocketAddr;
1726/// # }
1727/// #
1728/// # fn example(mut state: MyState, requests: Vec<FortressRequest<MyConfig>>) {
1729/// handle_requests!(
1730/// requests,
1731/// save: |_, _| { /* Never called in lockstep */ },
1732/// load: |_, _| { /* Never called in lockstep */ },
1733/// advance: |inputs: InputVec<MyInput>| {
1734/// state.frame += 1;
1735/// }
1736/// );
1737/// # }
1738/// ```
1739///
1740/// # Exhaustive Matching
1741///
1742/// `FortressRequest` is exhaustively matchable (not `#[non_exhaustive]`), so this
1743/// macro handles all variants. If a new variant is added in a future version,
1744/// the compiler will notify you at compile time.
1745///
1746/// [`P2PSession::advance_frame`]: crate::P2PSession::advance_frame
1747/// [`FortressRequest::SaveGameState`]: crate::FortressRequest::SaveGameState
1748/// [`FortressRequest::LoadGameState`]: crate::FortressRequest::LoadGameState
1749/// [`FortressRequest::AdvanceFrame`]: crate::FortressRequest::AdvanceFrame
1750#[macro_export]
1751macro_rules! handle_requests {
1752 (
1753 $requests:expr,
1754 save: $save:expr,
1755 load: $load:expr,
1756 advance: $advance:expr
1757 $(,)?
1758 ) => {{
1759 for request in $requests {
1760 match request {
1761 $crate::FortressRequest::SaveGameState { cell, frame } => {
1762 #[allow(clippy::redundant_closure_call)]
1763 ($save)(cell, frame);
1764 },
1765 $crate::FortressRequest::LoadGameState { cell, frame } => {
1766 #[allow(clippy::redundant_closure_call)]
1767 ($load)(cell, frame);
1768 },
1769 $crate::FortressRequest::AdvanceFrame { inputs } => {
1770 #[allow(clippy::redundant_closure_call)]
1771 ($advance)(inputs);
1772 },
1773 }
1774 }
1775 }};
1776}
1777
1778// #############
1779// # TRAITS #
1780// #############
1781
1782// special thanks to james7132 for the idea of a config trait that bundles all generics
1783
1784/// Compile time parameterization for sessions.
1785///
1786/// This trait bundles the generic types needed for a session. Implement this on
1787/// a marker struct to configure your session types.
1788///
1789/// # Example
1790///
1791/// ```
1792/// use fortress_rollback::Config;
1793/// use serde::{Deserialize, Serialize};
1794/// use std::net::SocketAddr;
1795///
1796/// // Your game's input type
1797/// #[derive(Copy, Clone, PartialEq, Default, Serialize, Deserialize)]
1798/// struct GameInput {
1799/// buttons: u8,
1800/// stick_x: i8,
1801/// stick_y: i8,
1802/// }
1803///
1804/// // Your game's state (for save/load)
1805/// #[derive(Clone)]
1806/// struct GameState {
1807/// frame: i32,
1808/// // ... game-specific state
1809/// }
1810///
1811/// // Marker struct for Config
1812/// struct GameConfig;
1813///
1814/// impl Config for GameConfig {
1815/// type Input = GameInput;
1816/// type State = GameState;
1817/// type Address = SocketAddr; // Most common choice for UDP games
1818/// }
1819/// ```
1820///
1821/// # Common Patterns
1822///
1823/// - **UDP Games**: Use `std::net::SocketAddr` for `Address`
1824/// - **WebRTC/Browser**: Use a custom address type from your WebRTC library
1825/// - **Local Testing**: Any `Clone + PartialEq + Eq + Ord + Hash + Debug` type works
1826#[cfg(feature = "sync-send")]
1827pub trait Config: 'static + Send + Sync {
1828 /// The input type for a session. This is the only game-related data
1829 /// transmitted over the network.
1830 ///
1831 /// The implementation of [Default] is used for representing "no input" for
1832 /// a player, including when a player is disconnected.
1833 type Input: Copy + Clone + PartialEq + Default + Serialize + DeserializeOwned + Send + Sync;
1834
1835 /// The save state type for the session.
1836 type State: Clone + Send + Sync;
1837
1838 /// The address type which identifies the remote clients
1839 type Address: Clone + PartialEq + Eq + PartialOrd + Ord + Hash + Send + Sync + Debug;
1840}
1841
1842/// This [`NonBlockingSocket`] trait is used when you want to use Fortress Rollback with your own socket.
1843/// However you wish to send and receive messages, it should be implemented through these two methods.
1844/// Messages should be sent in an UDP-like fashion, unordered and unreliable.
1845/// Fortress Rollback has an internal protocol on top of this to make sure all important information is sent and received.
1846#[cfg(feature = "sync-send")]
1847pub trait NonBlockingSocket<A>: Send + Sync
1848where
1849 A: Clone + PartialEq + Eq + Hash + Send + Sync,
1850{
1851 /// Takes a [`Message`] and sends it to the given address.
1852 fn send_to(&mut self, msg: &Message, addr: &A);
1853
1854 /// This method should return all messages received since the last time this method was called.
1855 /// The pairs `(A, Message)` indicate from which address each packet was received.
1856 fn receive_all_messages(&mut self) -> Vec<(A, Message)>;
1857}
1858
1859/// Compile time parameterization for sessions.
1860#[cfg(not(feature = "sync-send"))]
1861pub trait Config: 'static {
1862 /// The input type for a session. This is the only game-related data
1863 /// transmitted over the network.
1864 ///
1865 /// The implementation of [Default] is used for representing "no input" for
1866 /// a player, including when a player is disconnected.
1867 type Input: Copy + Clone + PartialEq + Default + Serialize + DeserializeOwned;
1868
1869 /// The save state type for the session.
1870 type State;
1871
1872 /// The address type which identifies the remote clients
1873 type Address: Clone + PartialEq + Eq + PartialOrd + Ord + Hash + Debug;
1874}
1875
1876/// A trait for integrating custom socket implementations with Fortress Rollback.
1877///
1878/// However you wish to send and receive messages, it should be implemented through these two methods.
1879/// Messages should be sent in an UDP-like fashion, unordered and unreliable.
1880/// Fortress Rollback has an internal protocol on top of this to make sure all important information is sent and received.
1881#[cfg(not(feature = "sync-send"))]
1882pub trait NonBlockingSocket<A>
1883where
1884 A: Clone + PartialEq + Eq + Hash,
1885{
1886 /// Takes a [`Message`] and sends it to the given address.
1887 fn send_to(&mut self, msg: &Message, addr: &A);
1888
1889 /// This method should return all messages received since the last time this method was called.
1890 /// The pairs `(A, Message)` indicate from which address each packet was received.
1891 fn receive_all_messages(&mut self) -> Vec<(A, Message)>;
1892}
1893
1894// ###################
1895// # UNIT TESTS #
1896// ###################
1897
1898#[cfg(test)]
1899#[allow(
1900 clippy::panic,
1901 clippy::unwrap_used,
1902 clippy::expect_used,
1903 clippy::indexing_slicing
1904)]
1905mod tests {
1906 use super::*;
1907 use std::net::SocketAddr;
1908
1909 /// A minimal test configuration for unit testing.
1910 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1911 struct TestConfig;
1912
1913 impl Config for TestConfig {
1914 type Input = u8;
1915 type State = Vec<u8>;
1916 type Address = SocketAddr;
1917 }
1918
1919 fn test_addr(port: u16) -> SocketAddr {
1920 use std::net::{IpAddr, Ipv4Addr};
1921 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)
1922 }
1923
1924 // ==========================================
1925 // SessionState Tests
1926 // ==========================================
1927
1928 #[test]
1929 fn session_state_default_values_exist() {
1930 // Verify both variants are constructible
1931 assert!(matches!(
1932 SessionState::Synchronizing,
1933 SessionState::Synchronizing
1934 ));
1935 assert!(matches!(SessionState::Running, SessionState::Running));
1936 }
1937
1938 #[test]
1939 fn session_state_equality() {
1940 assert_eq!(SessionState::Synchronizing, SessionState::Synchronizing);
1941 assert_eq!(SessionState::Running, SessionState::Running);
1942 assert_ne!(SessionState::Synchronizing, SessionState::Running);
1943 }
1944
1945 #[test]
1946 fn session_state_clone() {
1947 let state = SessionState::Running;
1948 let cloned = state;
1949 assert_eq!(state, cloned);
1950 }
1951
1952 #[test]
1953 fn session_state_copy() {
1954 let state = SessionState::Synchronizing;
1955 let copied: SessionState = state;
1956 assert_eq!(state, copied);
1957 }
1958
1959 #[test]
1960 fn session_state_debug_format() {
1961 let sync = SessionState::Synchronizing;
1962 let running = SessionState::Running;
1963 assert_eq!(format!("{:?}", sync), "Synchronizing");
1964 assert_eq!(format!("{:?}", running), "Running");
1965 }
1966
1967 // ==========================================
1968 // InputStatus Tests
1969 // ==========================================
1970
1971 #[test]
1972 fn input_status_variants_exist() {
1973 // Verify all variants are constructible
1974 assert!(matches!(InputStatus::Confirmed, InputStatus::Confirmed));
1975 assert!(matches!(InputStatus::Predicted, InputStatus::Predicted));
1976 assert!(matches!(
1977 InputStatus::Disconnected,
1978 InputStatus::Disconnected
1979 ));
1980 }
1981
1982 #[test]
1983 fn input_status_equality() {
1984 assert_eq!(InputStatus::Confirmed, InputStatus::Confirmed);
1985 assert_eq!(InputStatus::Predicted, InputStatus::Predicted);
1986 assert_eq!(InputStatus::Disconnected, InputStatus::Disconnected);
1987 assert_ne!(InputStatus::Confirmed, InputStatus::Predicted);
1988 assert_ne!(InputStatus::Confirmed, InputStatus::Disconnected);
1989 assert_ne!(InputStatus::Predicted, InputStatus::Disconnected);
1990 }
1991
1992 #[test]
1993 fn input_status_clone() {
1994 let status = InputStatus::Predicted;
1995 let cloned = status;
1996 assert_eq!(status, cloned);
1997 }
1998
1999 #[test]
2000 fn input_status_copy() {
2001 let status = InputStatus::Confirmed;
2002 let copied: InputStatus = status;
2003 assert_eq!(status, copied);
2004 }
2005
2006 #[test]
2007 fn input_status_debug_format() {
2008 assert_eq!(format!("{:?}", InputStatus::Confirmed), "Confirmed");
2009 assert_eq!(format!("{:?}", InputStatus::Predicted), "Predicted");
2010 assert_eq!(format!("{:?}", InputStatus::Disconnected), "Disconnected");
2011 }
2012
2013 // ==========================================
2014 // FortressEvent Tests
2015 // ==========================================
2016
2017 #[test]
2018 fn fortress_event_synchronizing() {
2019 let event: FortressEvent<TestConfig> = FortressEvent::Synchronizing {
2020 addr: test_addr(8080),
2021 total: 5,
2022 count: 2,
2023 total_requests_sent: 3,
2024 elapsed_ms: 100,
2025 };
2026
2027 if let FortressEvent::Synchronizing {
2028 total,
2029 count,
2030 total_requests_sent,
2031 elapsed_ms,
2032 ..
2033 } = event
2034 {
2035 assert_eq!(total, 5);
2036 assert_eq!(count, 2);
2037 assert_eq!(total_requests_sent, 3);
2038 assert_eq!(elapsed_ms, 100);
2039 } else {
2040 panic!("Expected Synchronizing event");
2041 }
2042 }
2043
2044 #[test]
2045 fn fortress_event_synchronized() {
2046 let addr = test_addr(8080);
2047 let event: FortressEvent<TestConfig> = FortressEvent::Synchronized { addr };
2048
2049 if let FortressEvent::Synchronized { addr: received } = event {
2050 assert_eq!(received, addr);
2051 } else {
2052 panic!("Expected Synchronized event");
2053 }
2054 }
2055
2056 #[test]
2057 fn fortress_event_disconnected() {
2058 let addr = test_addr(9000);
2059 let event: FortressEvent<TestConfig> = FortressEvent::Disconnected { addr };
2060
2061 if let FortressEvent::Disconnected { addr: received } = event {
2062 assert_eq!(received, addr);
2063 } else {
2064 panic!("Expected Disconnected event");
2065 }
2066 }
2067
2068 #[test]
2069 fn fortress_event_network_interrupted() {
2070 let event: FortressEvent<TestConfig> = FortressEvent::NetworkInterrupted {
2071 addr: test_addr(8080),
2072 disconnect_timeout: 5000,
2073 };
2074
2075 if let FortressEvent::NetworkInterrupted {
2076 disconnect_timeout, ..
2077 } = event
2078 {
2079 assert_eq!(disconnect_timeout, 5000);
2080 } else {
2081 panic!("Expected NetworkInterrupted event");
2082 }
2083 }
2084
2085 #[test]
2086 fn fortress_event_network_resumed() {
2087 let addr = test_addr(8080);
2088 let event: FortressEvent<TestConfig> = FortressEvent::NetworkResumed { addr };
2089
2090 if let FortressEvent::NetworkResumed { addr: received } = event {
2091 assert_eq!(received, addr);
2092 } else {
2093 panic!("Expected NetworkResumed event");
2094 }
2095 }
2096
2097 #[test]
2098 fn fortress_event_wait_recommendation() {
2099 let event: FortressEvent<TestConfig> = FortressEvent::WaitRecommendation { skip_frames: 3 };
2100
2101 if let FortressEvent::WaitRecommendation { skip_frames } = event {
2102 assert_eq!(skip_frames, 3);
2103 } else {
2104 panic!("Expected WaitRecommendation event");
2105 }
2106 }
2107
2108 #[test]
2109 fn fortress_event_desync_detected() {
2110 let event: FortressEvent<TestConfig> = FortressEvent::DesyncDetected {
2111 frame: Frame::new(100),
2112 local_checksum: 0x1234,
2113 remote_checksum: 0x5678,
2114 addr: test_addr(8080),
2115 };
2116
2117 if let FortressEvent::DesyncDetected {
2118 frame,
2119 local_checksum,
2120 remote_checksum,
2121 ..
2122 } = event
2123 {
2124 assert_eq!(frame, Frame::new(100));
2125 assert_eq!(local_checksum, 0x1234);
2126 assert_eq!(remote_checksum, 0x5678);
2127 } else {
2128 panic!("Expected DesyncDetected event");
2129 }
2130 }
2131
2132 #[test]
2133 fn fortress_event_sync_timeout() {
2134 let event: FortressEvent<TestConfig> = FortressEvent::SyncTimeout {
2135 addr: test_addr(8080),
2136 elapsed_ms: 10000,
2137 };
2138
2139 if let FortressEvent::SyncTimeout { elapsed_ms, .. } = event {
2140 assert_eq!(elapsed_ms, 10000);
2141 } else {
2142 panic!("Expected SyncTimeout event");
2143 }
2144 }
2145
2146 #[test]
2147 fn fortress_event_equality() {
2148 let event1: FortressEvent<TestConfig> =
2149 FortressEvent::WaitRecommendation { skip_frames: 5 };
2150 let event2: FortressEvent<TestConfig> =
2151 FortressEvent::WaitRecommendation { skip_frames: 5 };
2152 let event3: FortressEvent<TestConfig> =
2153 FortressEvent::WaitRecommendation { skip_frames: 10 };
2154
2155 assert_eq!(event1, event2);
2156 assert_ne!(event1, event3);
2157 }
2158
2159 #[test]
2160 fn fortress_event_clone() {
2161 let event: FortressEvent<TestConfig> = FortressEvent::Synchronized {
2162 addr: test_addr(8080),
2163 };
2164 let cloned = event;
2165 assert_eq!(event, cloned);
2166 }
2167
2168 #[test]
2169 fn fortress_event_debug_format() {
2170 let event: FortressEvent<TestConfig> = FortressEvent::WaitRecommendation { skip_frames: 3 };
2171 let debug = format!("{:?}", event);
2172 assert!(debug.contains("WaitRecommendation"));
2173 assert!(debug.contains('3'));
2174 }
2175
2176 // ==========================================
2177 // FortressEvent Display Tests
2178 // ==========================================
2179
2180 #[test]
2181 fn fortress_event_display_synchronizing() {
2182 let event: FortressEvent<TestConfig> = FortressEvent::Synchronizing {
2183 addr: test_addr(8080),
2184 total: 5,
2185 count: 2,
2186 total_requests_sent: 3,
2187 elapsed_ms: 100,
2188 };
2189 let display = event.to_string();
2190 assert!(display.starts_with("Synchronizing("));
2191 assert!(display.contains("2/5"));
2192 assert!(display.contains("127.0.0.1:8080"));
2193 assert!(display.contains("requests_sent=3"));
2194 assert!(display.contains("elapsed=100ms"));
2195 }
2196
2197 #[test]
2198 fn fortress_event_display_synchronized() {
2199 let event: FortressEvent<TestConfig> = FortressEvent::Synchronized {
2200 addr: test_addr(9000),
2201 };
2202 assert_eq!(event.to_string(), "Synchronized(addr=127.0.0.1:9000)");
2203 }
2204
2205 #[test]
2206 fn fortress_event_display_disconnected() {
2207 let event: FortressEvent<TestConfig> = FortressEvent::Disconnected {
2208 addr: test_addr(7000),
2209 };
2210 assert_eq!(event.to_string(), "Disconnected(addr=127.0.0.1:7000)");
2211 }
2212
2213 #[test]
2214 fn fortress_event_display_network_interrupted() {
2215 let event: FortressEvent<TestConfig> = FortressEvent::NetworkInterrupted {
2216 addr: test_addr(8080),
2217 disconnect_timeout: 5000,
2218 };
2219 let display = event.to_string();
2220 assert!(display.starts_with("NetworkInterrupted("));
2221 assert!(display.contains("127.0.0.1:8080"));
2222 assert!(display.contains("timeout=5000ms"));
2223 }
2224
2225 #[test]
2226 fn fortress_event_display_network_resumed() {
2227 let event: FortressEvent<TestConfig> = FortressEvent::NetworkResumed {
2228 addr: test_addr(8080),
2229 };
2230 assert_eq!(event.to_string(), "NetworkResumed(addr=127.0.0.1:8080)");
2231 }
2232
2233 #[test]
2234 fn fortress_event_display_wait_recommendation() {
2235 let event: FortressEvent<TestConfig> = FortressEvent::WaitRecommendation { skip_frames: 3 };
2236 assert_eq!(event.to_string(), "WaitRecommendation(skip_frames=3)");
2237 }
2238
2239 #[test]
2240 fn fortress_event_display_desync_detected() {
2241 let event: FortressEvent<TestConfig> = FortressEvent::DesyncDetected {
2242 frame: Frame::new(100),
2243 local_checksum: 0x1234,
2244 remote_checksum: 0x5678,
2245 addr: test_addr(8080),
2246 };
2247 let display = event.to_string();
2248 assert!(display.starts_with("DesyncDetected("));
2249 assert!(display.contains("frame=100"));
2250 assert!(display.contains("local=0x1234"));
2251 assert!(display.contains("remote=0x5678"));
2252 assert!(display.contains("127.0.0.1:8080"));
2253 }
2254
2255 #[test]
2256 fn fortress_event_display_sync_timeout() {
2257 let event: FortressEvent<TestConfig> = FortressEvent::SyncTimeout {
2258 addr: test_addr(8080),
2259 elapsed_ms: 10000,
2260 };
2261 let display = event.to_string();
2262 assert!(display.starts_with("SyncTimeout("));
2263 assert!(display.contains("127.0.0.1:8080"));
2264 assert!(display.contains("elapsed=10000ms"));
2265 }
2266
2267 // ==========================================
2268 // SessionState Display Tests
2269 // ==========================================
2270
2271 #[test]
2272 fn session_state_display_synchronizing() {
2273 assert_eq!(SessionState::Synchronizing.to_string(), "Synchronizing");
2274 }
2275
2276 #[test]
2277 fn session_state_display_running() {
2278 assert_eq!(SessionState::Running.to_string(), "Running");
2279 }
2280
2281 // ==========================================
2282 // InputStatus Display Tests
2283 // ==========================================
2284
2285 #[test]
2286 fn input_status_display_confirmed() {
2287 assert_eq!(InputStatus::Confirmed.to_string(), "Confirmed");
2288 }
2289
2290 #[test]
2291 fn input_status_display_predicted() {
2292 assert_eq!(InputStatus::Predicted.to_string(), "Predicted");
2293 }
2294
2295 #[test]
2296 fn input_status_display_disconnected() {
2297 assert_eq!(InputStatus::Disconnected.to_string(), "Disconnected");
2298 }
2299
2300 // ==========================================
2301 // Frame Display Tests
2302 // ==========================================
2303
2304 #[test]
2305 fn frame_display_valid() {
2306 assert_eq!(Frame::new(42).to_string(), "42");
2307 assert_eq!(Frame::new(0).to_string(), "0");
2308 assert_eq!(Frame::new(12345).to_string(), "12345");
2309 }
2310
2311 #[test]
2312 fn frame_display_null() {
2313 assert_eq!(Frame::NULL.to_string(), "NULL_FRAME");
2314 }
2315
2316 #[test]
2317 fn frame_display_negative() {
2318 // Negative frames other than NULL show as-is
2319 assert_eq!(Frame::new(-5).to_string(), "-5");
2320 }
2321
2322 // ==========================================
2323 // DesyncDetection Display Tests
2324 // ==========================================
2325
2326 #[test]
2327 fn desync_detection_display_on() {
2328 let detection = DesyncDetection::On { interval: 60 };
2329 assert_eq!(detection.to_string(), "On(interval=60)");
2330 }
2331
2332 #[test]
2333 fn desync_detection_display_on_custom_interval() {
2334 let detection = DesyncDetection::On { interval: 1 };
2335 assert_eq!(detection.to_string(), "On(interval=1)");
2336 }
2337
2338 #[test]
2339 fn desync_detection_display_off() {
2340 assert_eq!(DesyncDetection::Off.to_string(), "Off");
2341 }
2342
2343 // ==========================================
2344 // PlayerType Tests
2345 // ==========================================
2346
2347 #[test]
2348 fn player_type_local() {
2349 let player_type: PlayerType<SocketAddr> = PlayerType::Local;
2350 assert!(matches!(player_type, PlayerType::Local));
2351 }
2352
2353 #[test]
2354 fn player_type_remote() {
2355 let addr = test_addr(8080);
2356 let player_type: PlayerType<SocketAddr> = PlayerType::Remote(addr);
2357
2358 if let PlayerType::Remote(received) = player_type {
2359 assert_eq!(received, addr);
2360 } else {
2361 panic!("Expected Remote player type");
2362 }
2363 }
2364
2365 #[test]
2366 fn player_type_spectator() {
2367 let addr = test_addr(9000);
2368 let player_type: PlayerType<SocketAddr> = PlayerType::Spectator(addr);
2369
2370 if let PlayerType::Spectator(received) = player_type {
2371 assert_eq!(received, addr);
2372 } else {
2373 panic!("Expected Spectator player type");
2374 }
2375 }
2376
2377 #[test]
2378 fn player_type_equality() {
2379 let addr1 = test_addr(8080);
2380 let addr2 = test_addr(9000);
2381
2382 assert_eq!(
2383 PlayerType::<SocketAddr>::Local,
2384 PlayerType::<SocketAddr>::Local
2385 );
2386 assert_eq!(PlayerType::Remote(addr1), PlayerType::Remote(addr1));
2387 assert_ne!(PlayerType::Remote(addr1), PlayerType::Remote(addr2));
2388 assert_ne!(PlayerType::<SocketAddr>::Local, PlayerType::Remote(addr1));
2389 }
2390
2391 #[test]
2392 fn player_type_clone() {
2393 let player_type: PlayerType<SocketAddr> = PlayerType::Remote(test_addr(8080));
2394 let cloned = player_type; // PlayerType is Copy
2395 assert_eq!(player_type, cloned);
2396 }
2397
2398 #[test]
2399 fn player_type_debug_format() {
2400 let local: PlayerType<SocketAddr> = PlayerType::Local;
2401 assert_eq!(format!("{:?}", local), "Local");
2402
2403 let remote: PlayerType<SocketAddr> = PlayerType::Remote(test_addr(8080));
2404 let debug = format!("{:?}", remote);
2405 assert!(debug.contains("Remote"));
2406 }
2407
2408 // ==========================================
2409 // PlayerHandle Tests
2410 // ==========================================
2411
2412 #[test]
2413 fn player_handle_new() {
2414 let handle = PlayerHandle::new(0);
2415 assert_eq!(handle.as_usize(), 0);
2416
2417 let handle = PlayerHandle::new(5);
2418 assert_eq!(handle.as_usize(), 5);
2419 }
2420
2421 #[test]
2422 fn player_handle_is_valid_player_for() {
2423 let handle = PlayerHandle::new(0);
2424 assert!(handle.is_valid_player_for(2));
2425 assert!(handle.is_valid_player_for(1));
2426 assert!(!handle.is_valid_player_for(0));
2427
2428 let handle = PlayerHandle::new(5);
2429 assert!(handle.is_valid_player_for(6));
2430 assert!(!handle.is_valid_player_for(5));
2431 assert!(!handle.is_valid_player_for(4));
2432 }
2433
2434 #[test]
2435 fn player_handle_is_spectator_for() {
2436 let handle = PlayerHandle::new(0);
2437 assert!(!handle.is_spectator_for(2));
2438
2439 let handle = PlayerHandle::new(2);
2440 assert!(handle.is_spectator_for(2));
2441 assert!(!handle.is_spectator_for(3));
2442 }
2443
2444 #[test]
2445 fn player_handle_equality() {
2446 let handle1 = PlayerHandle::new(1);
2447 let handle2 = PlayerHandle::new(1);
2448 let handle3 = PlayerHandle::new(2);
2449
2450 assert_eq!(handle1, handle2);
2451 assert_ne!(handle1, handle3);
2452 }
2453
2454 #[test]
2455 fn player_handle_hash() {
2456 use std::collections::HashSet;
2457 let mut set = HashSet::new();
2458 set.insert(PlayerHandle::new(0));
2459 set.insert(PlayerHandle::new(1));
2460 set.insert(PlayerHandle::new(0)); // duplicate
2461
2462 assert_eq!(set.len(), 2);
2463 }
2464
2465 #[test]
2466 fn player_handle_ordering() {
2467 let h0 = PlayerHandle::new(0);
2468 let h1 = PlayerHandle::new(1);
2469 let h2 = PlayerHandle::new(2);
2470
2471 assert!(h0 < h1);
2472 assert!(h1 < h2);
2473 assert!(h0 < h2);
2474 }
2475
2476 #[test]
2477 fn player_handle_debug_format() {
2478 let handle = PlayerHandle::new(3);
2479 let debug = format!("{:?}", handle);
2480 assert!(debug.contains('3'));
2481 }
2482
2483 #[test]
2484 fn player_handle_display_format() {
2485 let handle = PlayerHandle::new(0);
2486 assert_eq!(format!("{}", handle), "PlayerHandle(0)");
2487
2488 let handle = PlayerHandle::new(5);
2489 assert_eq!(format!("{}", handle), "PlayerHandle(5)");
2490
2491 let handle = PlayerHandle::new(42);
2492 assert_eq!(format!("{}", handle), "PlayerHandle(42)");
2493 }
2494
2495 // ==========================================
2496 // PlayerType Display Tests
2497 // ==========================================
2498
2499 #[test]
2500 fn player_type_display_local() {
2501 let player: PlayerType<SocketAddr> = PlayerType::Local;
2502 assert_eq!(format!("{}", player), "Local");
2503 }
2504
2505 #[test]
2506 fn player_type_display_remote() {
2507 let addr = test_addr(8080);
2508 let player: PlayerType<SocketAddr> = PlayerType::Remote(addr);
2509 let display = format!("{}", player);
2510 assert!(display.starts_with("Remote("));
2511 assert!(display.contains("127.0.0.1:8080"));
2512 }
2513
2514 #[test]
2515 fn player_type_display_spectator() {
2516 let addr = test_addr(9000);
2517 let player: PlayerType<SocketAddr> = PlayerType::Spectator(addr);
2518 let display = format!("{}", player);
2519 assert!(display.starts_with("Spectator("));
2520 assert!(display.contains("127.0.0.1:9000"));
2521 }
2522
2523 // ==========================================
2524 // FortressRequest Display Tests
2525 // ==========================================
2526
2527 #[test]
2528 fn fortress_request_display_save_game_state() {
2529 let cell = GameStateCell::<Vec<u8>>::default();
2530 let request: FortressRequest<TestConfig> = FortressRequest::SaveGameState {
2531 cell,
2532 frame: Frame::new(100),
2533 };
2534 let display = format!("{}", request);
2535 assert_eq!(display, "SaveGameState(frame=100)");
2536 }
2537
2538 #[test]
2539 fn fortress_request_display_load_game_state() {
2540 let cell = GameStateCell::<Vec<u8>>::default();
2541 let request: FortressRequest<TestConfig> = FortressRequest::LoadGameState {
2542 cell,
2543 frame: Frame::new(50),
2544 };
2545 let display = format!("{}", request);
2546 assert_eq!(display, "LoadGameState(frame=50)");
2547 }
2548
2549 #[test]
2550 fn fortress_request_display_advance_frame() {
2551 use crate::InputVec;
2552 let inputs: InputVec<u8> = smallvec::smallvec![
2553 (1_u8, InputStatus::Confirmed),
2554 (2_u8, InputStatus::Predicted),
2555 ];
2556 let request: FortressRequest<TestConfig> = FortressRequest::AdvanceFrame { inputs };
2557 let display = format!("{}", request);
2558 assert_eq!(display, "AdvanceFrame(inputs=2)");
2559 }
2560
2561 #[test]
2562 fn fortress_request_display_advance_frame_empty() {
2563 use crate::InputVec;
2564 let inputs: InputVec<u8> = smallvec::smallvec![];
2565 let request: FortressRequest<TestConfig> = FortressRequest::AdvanceFrame { inputs };
2566 let display = format!("{}", request);
2567 assert_eq!(display, "AdvanceFrame(inputs=0)");
2568 }
2569
2570 // ==========================================
2571 // Frame Tests (additional tests beyond kani)
2572 // ==========================================
2573
2574 #[test]
2575 fn frame_null_constant() {
2576 assert_eq!(Frame::NULL.as_i32(), -1);
2577 assert!(Frame::NULL.is_null());
2578 assert!(!Frame::NULL.is_valid());
2579 }
2580
2581 #[test]
2582 fn frame_new() {
2583 let frame = Frame::new(0);
2584 assert_eq!(frame.as_i32(), 0);
2585 assert!(!frame.is_null());
2586 assert!(frame.is_valid());
2587
2588 let frame = Frame::new(100);
2589 assert_eq!(frame.as_i32(), 100);
2590 }
2591
2592 #[test]
2593 fn frame_arithmetic() {
2594 let frame = Frame::new(10);
2595
2596 // Addition with i32
2597 assert_eq!((frame + 5).as_i32(), 15);
2598
2599 // Subtraction with i32
2600 assert_eq!((frame - 3).as_i32(), 7);
2601
2602 // Subtraction between frames
2603 assert_eq!(Frame::new(10) - Frame::new(5), 5);
2604 }
2605
2606 #[test]
2607 fn frame_add_assign() {
2608 let mut frame = Frame::new(10);
2609 frame += 5;
2610 assert_eq!(frame.as_i32(), 15);
2611 }
2612
2613 #[test]
2614 fn frame_sub_assign() {
2615 let mut frame = Frame::new(10);
2616 frame -= 3;
2617 assert_eq!(frame.as_i32(), 7);
2618 }
2619
2620 #[test]
2621 fn frame_comparison() {
2622 let f1 = Frame::new(5);
2623 let f2 = Frame::new(10);
2624 let f3 = Frame::new(5);
2625
2626 assert!(f1 < f2);
2627 assert!(f2 > f1);
2628 assert!(f1 <= f3);
2629 assert!(f1 >= f3);
2630 assert_eq!(f1, f3);
2631 }
2632
2633 #[test]
2634 fn frame_modulo() {
2635 let frame = Frame::new(135);
2636 let remainder = frame % 128;
2637 assert_eq!(remainder, 7);
2638 }
2639
2640 #[test]
2641 fn frame_to_option() {
2642 assert!(Frame::NULL.to_option().is_none());
2643 assert_eq!(Frame::new(5).to_option(), Some(Frame::new(5)));
2644 }
2645
2646 #[test]
2647 fn frame_from_option() {
2648 assert_eq!(Frame::from_option(None), Frame::NULL);
2649 assert_eq!(Frame::from_option(Some(Frame::new(5))), Frame::new(5));
2650 }
2651
2652 #[test]
2653 fn frame_debug_format() {
2654 let frame = Frame::new(42);
2655 let debug = format!("{:?}", frame);
2656 // Use multi-char string to avoid single_char_pattern lint
2657 assert!(debug.contains("42"));
2658 }
2659
2660 // ==========================================
2661 // Frame Checked/Saturating Arithmetic Tests
2662 // ==========================================
2663
2664 #[test]
2665 fn frame_checked_add_normal() {
2666 let frame = Frame::new(100);
2667 assert_eq!(frame.checked_add(50), Some(Frame::new(150)));
2668 assert_eq!(frame.checked_add(-50), Some(Frame::new(50)));
2669 assert_eq!(frame.checked_add(0), Some(frame));
2670 }
2671
2672 #[test]
2673 fn frame_checked_add_overflow() {
2674 let frame = Frame::new(i32::MAX);
2675 assert_eq!(frame.checked_add(1), None);
2676 assert_eq!(frame.checked_add(100), None);
2677
2678 // Underflow case
2679 let frame = Frame::new(i32::MIN);
2680 assert_eq!(frame.checked_add(-1), None);
2681 }
2682
2683 #[test]
2684 fn frame_checked_sub_normal() {
2685 let frame = Frame::new(100);
2686 assert_eq!(frame.checked_sub(50), Some(Frame::new(50)));
2687 assert_eq!(frame.checked_sub(-50), Some(Frame::new(150)));
2688 assert_eq!(frame.checked_sub(0), Some(frame));
2689 }
2690
2691 #[test]
2692 fn frame_checked_sub_overflow() {
2693 let frame = Frame::new(i32::MIN);
2694 assert_eq!(frame.checked_sub(1), None);
2695
2696 let frame = Frame::new(i32::MAX);
2697 assert_eq!(frame.checked_sub(-1), None);
2698 }
2699
2700 #[test]
2701 fn frame_saturating_add_normal() {
2702 let frame = Frame::new(100);
2703 assert_eq!(frame.saturating_add(50), Frame::new(150));
2704 assert_eq!(frame.saturating_add(-50), Frame::new(50));
2705 }
2706
2707 #[test]
2708 fn frame_saturating_add_clamps_at_max() {
2709 let frame = Frame::new(i32::MAX);
2710 assert_eq!(frame.saturating_add(1), Frame::new(i32::MAX));
2711 assert_eq!(frame.saturating_add(100), Frame::new(i32::MAX));
2712 }
2713
2714 #[test]
2715 fn frame_saturating_add_clamps_at_min() {
2716 let frame = Frame::new(i32::MIN);
2717 assert_eq!(frame.saturating_add(-1), Frame::new(i32::MIN));
2718 assert_eq!(frame.saturating_add(-100), Frame::new(i32::MIN));
2719 }
2720
2721 #[test]
2722 fn frame_saturating_sub_normal() {
2723 let frame = Frame::new(100);
2724 assert_eq!(frame.saturating_sub(50), Frame::new(50));
2725 assert_eq!(frame.saturating_sub(-50), Frame::new(150));
2726 }
2727
2728 #[test]
2729 fn frame_saturating_sub_clamps_at_min() {
2730 let frame = Frame::new(i32::MIN);
2731 assert_eq!(frame.saturating_sub(1), Frame::new(i32::MIN));
2732 }
2733
2734 #[test]
2735 fn frame_saturating_sub_clamps_at_max() {
2736 let frame = Frame::new(i32::MAX);
2737 assert_eq!(frame.saturating_sub(-1), Frame::new(i32::MAX));
2738 }
2739
2740 #[test]
2741 fn frame_abs_diff_basic() {
2742 let f1 = Frame::new(10);
2743 let f2 = Frame::new(15);
2744
2745 // Order-independent
2746 assert_eq!(f1.abs_diff(f2), 5);
2747 assert_eq!(f2.abs_diff(f1), 5);
2748
2749 // Same frame
2750 assert_eq!(f1.abs_diff(f1), 0);
2751 }
2752
2753 #[test]
2754 fn frame_abs_diff_extremes() {
2755 // Large positive difference
2756 let f1 = Frame::new(0);
2757 let f2 = Frame::new(i32::MAX);
2758 assert_eq!(f1.abs_diff(f2), i32::MAX as u32);
2759
2760 // With NULL frame (-1)
2761 let null = Frame::NULL;
2762 let zero = Frame::new(0);
2763 assert_eq!(null.abs_diff(zero), 1);
2764 }
2765
2766 // ==========================================
2767 // Safe Frame Macro Tests
2768 // ==========================================
2769
2770 #[test]
2771 fn safe_frame_add_normal_operation() {
2772 let frame = Frame::new(100);
2773 let result = safe_frame_add!(frame, 50, "test_normal_add");
2774 assert_eq!(result, Frame::new(150));
2775 }
2776
2777 #[test]
2778 fn safe_frame_add_returns_saturated_on_overflow() {
2779 let frame = Frame::new(i32::MAX);
2780 let result = safe_frame_add!(frame, 1, "test_overflow_add");
2781 // Should return saturated value (max) instead of panicking
2782 assert_eq!(result, Frame::new(i32::MAX));
2783 }
2784
2785 #[test]
2786 fn safe_frame_sub_normal_operation() {
2787 let frame = Frame::new(100);
2788 let result = safe_frame_sub!(frame, 50, "test_normal_sub");
2789 assert_eq!(result, Frame::new(50));
2790 }
2791
2792 #[test]
2793 fn safe_frame_sub_returns_saturated_on_underflow() {
2794 let frame = Frame::new(i32::MIN);
2795 let result = safe_frame_sub!(frame, 1, "test_underflow_sub");
2796 // Should return saturated value (min) instead of panicking
2797 assert_eq!(result, Frame::new(i32::MIN));
2798 }
2799
2800 #[test]
2801 fn safe_frame_macros_accept_negative_deltas() {
2802 let frame = Frame::new(100);
2803
2804 // Negative add = subtract
2805 let result = safe_frame_add!(frame, -25, "test_negative_add");
2806 assert_eq!(result, Frame::new(75));
2807
2808 // Negative sub = add
2809 let result = safe_frame_sub!(frame, -25, "test_negative_sub");
2810 assert_eq!(result, Frame::new(125));
2811 }
2812
2813 // ==========================================
2814 // Frame Ergonomic Methods Tests
2815 // ==========================================
2816
2817 #[test]
2818 fn frame_as_usize_positive() {
2819 assert_eq!(Frame::new(0).as_usize(), Some(0));
2820 assert_eq!(Frame::new(42).as_usize(), Some(42));
2821 assert_eq!(Frame::new(i32::MAX).as_usize(), Some(i32::MAX as usize));
2822 }
2823
2824 #[test]
2825 fn frame_as_usize_negative() {
2826 assert_eq!(Frame::NULL.as_usize(), None);
2827 assert_eq!(Frame::new(-1).as_usize(), None);
2828 assert_eq!(Frame::new(-100).as_usize(), None);
2829 assert_eq!(Frame::new(i32::MIN).as_usize(), None);
2830 }
2831
2832 #[test]
2833 fn frame_try_as_usize_positive() {
2834 assert_eq!(Frame::new(0).try_as_usize().unwrap(), 0);
2835 assert_eq!(Frame::new(42).try_as_usize().unwrap(), 42);
2836 }
2837
2838 #[test]
2839 fn frame_try_as_usize_negative_returns_error() {
2840 let err = Frame::NULL.try_as_usize().unwrap_err();
2841 assert!(matches!(
2842 err,
2843 FortressError::InvalidFrameStructured {
2844 reason: InvalidFrameReason::MustBeNonNegative,
2845 ..
2846 }
2847 ));
2848 }
2849
2850 #[test]
2851 fn frame_buffer_index_basic() {
2852 assert_eq!(Frame::new(0).buffer_index(4), Some(0));
2853 assert_eq!(Frame::new(1).buffer_index(4), Some(1));
2854 assert_eq!(Frame::new(4).buffer_index(4), Some(0));
2855 assert_eq!(Frame::new(7).buffer_index(4), Some(3));
2856 assert_eq!(Frame::new(100).buffer_index(8), Some(4)); // 100 % 8 = 4
2857 }
2858
2859 #[test]
2860 fn frame_buffer_index_negative_frame() {
2861 assert_eq!(Frame::NULL.buffer_index(4), None);
2862 assert_eq!(Frame::new(-5).buffer_index(4), None);
2863 }
2864
2865 #[test]
2866 fn frame_buffer_index_zero_size() {
2867 assert_eq!(Frame::new(5).buffer_index(0), None);
2868 assert_eq!(Frame::new(0).buffer_index(0), None);
2869 }
2870
2871 #[test]
2872 fn frame_try_buffer_index_success() {
2873 assert_eq!(Frame::new(0).try_buffer_index(4).unwrap(), 0);
2874 assert_eq!(Frame::new(1).try_buffer_index(4).unwrap(), 1);
2875 assert_eq!(Frame::new(4).try_buffer_index(4).unwrap(), 0);
2876 assert_eq!(Frame::new(7).try_buffer_index(4).unwrap(), 3);
2877 assert_eq!(Frame::new(100).try_buffer_index(8).unwrap(), 4); // 100 % 8 = 4
2878 }
2879
2880 #[test]
2881 fn frame_try_buffer_index_negative_frame() {
2882 let err = Frame::NULL.try_buffer_index(4).unwrap_err();
2883 assert!(matches!(err, FortressError::InvalidFrameStructured { .. }));
2884
2885 let err = Frame::new(-5).try_buffer_index(4).unwrap_err();
2886 assert!(matches!(err, FortressError::InvalidFrameStructured { .. }));
2887 }
2888
2889 #[test]
2890 fn frame_try_buffer_index_zero_size() {
2891 let err = Frame::new(5).try_buffer_index(0).unwrap_err();
2892 assert!(matches!(
2893 err,
2894 FortressError::InvalidRequestStructured {
2895 kind: InvalidRequestKind::ZeroBufferSize
2896 }
2897 ));
2898
2899 // Verify ZeroBufferSize error takes precedence even with Frame::new(0)
2900 let err = Frame::new(0).try_buffer_index(0).unwrap_err();
2901 assert!(matches!(
2902 err,
2903 FortressError::InvalidRequestStructured {
2904 kind: InvalidRequestKind::ZeroBufferSize
2905 }
2906 ));
2907 }
2908
2909 #[test]
2910 fn frame_try_add_success() {
2911 let frame = Frame::new(100);
2912 assert_eq!(frame.try_add(50).unwrap(), Frame::new(150));
2913 assert_eq!(frame.try_add(-50).unwrap(), Frame::new(50));
2914 assert_eq!(frame.try_add(0).unwrap(), Frame::new(100));
2915 }
2916
2917 #[test]
2918 fn frame_try_add_overflow() {
2919 let err = Frame::new(i32::MAX).try_add(1).unwrap_err();
2920 assert!(matches!(
2921 err,
2922 FortressError::FrameArithmeticOverflow {
2923 operation: "add",
2924 operand: 1,
2925 ..
2926 }
2927 ));
2928 }
2929
2930 #[test]
2931 fn frame_try_sub_success() {
2932 let frame = Frame::new(100);
2933 assert_eq!(frame.try_sub(50).unwrap(), Frame::new(50));
2934 assert_eq!(frame.try_sub(-50).unwrap(), Frame::new(150));
2935 }
2936
2937 #[test]
2938 fn frame_try_sub_overflow() {
2939 let err = Frame::new(i32::MIN).try_sub(1).unwrap_err();
2940 assert!(matches!(
2941 err,
2942 FortressError::FrameArithmeticOverflow {
2943 operation: "sub",
2944 operand: 1,
2945 ..
2946 }
2947 ));
2948 }
2949
2950 #[test]
2951 fn frame_next_success() {
2952 assert_eq!(Frame::new(0).next().unwrap(), Frame::new(1));
2953 assert_eq!(Frame::new(100).next().unwrap(), Frame::new(101));
2954 }
2955
2956 #[test]
2957 fn frame_next_overflow() {
2958 assert!(Frame::new(i32::MAX).next().is_err());
2959 }
2960
2961 #[test]
2962 fn frame_prev_success() {
2963 assert_eq!(Frame::new(1).prev().unwrap(), Frame::new(0));
2964 assert_eq!(Frame::new(100).prev().unwrap(), Frame::new(99));
2965 }
2966
2967 #[test]
2968 fn frame_prev_overflow() {
2969 assert!(Frame::new(i32::MIN).prev().is_err());
2970 }
2971
2972 #[test]
2973 fn frame_saturating_next() {
2974 assert_eq!(Frame::new(0).saturating_next(), Frame::new(1));
2975 assert_eq!(Frame::new(100).saturating_next(), Frame::new(101));
2976 assert_eq!(Frame::new(i32::MAX).saturating_next(), Frame::new(i32::MAX));
2977 }
2978
2979 #[test]
2980 fn frame_saturating_prev() {
2981 assert_eq!(Frame::new(1).saturating_prev(), Frame::new(0));
2982 assert_eq!(Frame::new(100).saturating_prev(), Frame::new(99));
2983 assert_eq!(Frame::new(i32::MIN).saturating_prev(), Frame::new(i32::MIN));
2984 }
2985
2986 #[test]
2987 fn frame_from_usize_valid() {
2988 assert_eq!(Frame::from_usize(0), Some(Frame::new(0)));
2989 assert_eq!(Frame::from_usize(42), Some(Frame::new(42)));
2990 assert_eq!(
2991 Frame::from_usize(i32::MAX as usize),
2992 Some(Frame::new(i32::MAX))
2993 );
2994 }
2995
2996 #[test]
2997 fn frame_from_usize_too_large() {
2998 let too_large = (i32::MAX as usize) + 1;
2999 assert_eq!(Frame::from_usize(too_large), None);
3000 assert_eq!(Frame::from_usize(usize::MAX), None);
3001 }
3002
3003 #[test]
3004 fn frame_try_from_usize_valid() {
3005 assert_eq!(Frame::try_from_usize(0).unwrap(), Frame::new(0));
3006 assert_eq!(Frame::try_from_usize(42).unwrap(), Frame::new(42));
3007 }
3008
3009 #[test]
3010 fn frame_try_from_usize_too_large() {
3011 let too_large = (i32::MAX as usize) + 1;
3012 let err = Frame::try_from_usize(too_large).unwrap_err();
3013 assert!(matches!(
3014 err,
3015 FortressError::FrameValueTooLarge { value } if value == too_large
3016 ));
3017 }
3018
3019 #[test]
3020 fn frame_distance_to_basic() {
3021 let a = Frame::new(100);
3022 let b = Frame::new(150);
3023
3024 assert_eq!(a.distance_to(b), Some(50));
3025 assert_eq!(b.distance_to(a), Some(-50));
3026 assert_eq!(a.distance_to(a), Some(0));
3027 }
3028
3029 #[test]
3030 fn frame_distance_to_with_negative_frames() {
3031 let a = Frame::new(-10);
3032 let b = Frame::new(10);
3033 assert_eq!(a.distance_to(b), Some(20));
3034 assert_eq!(b.distance_to(a), Some(-20));
3035 }
3036
3037 #[test]
3038 fn frame_distance_to_overflow() {
3039 // This would overflow: i32::MAX - i32::MIN
3040 assert_eq!(Frame::new(i32::MIN).distance_to(Frame::new(i32::MAX)), None);
3041 assert_eq!(Frame::new(i32::MAX).distance_to(Frame::new(i32::MIN)), None);
3042 }
3043
3044 #[test]
3045 fn frame_is_within_inside() {
3046 let reference = Frame::new(100);
3047 assert!(Frame::new(100).is_within(5, reference)); // diff = 0
3048 assert!(Frame::new(98).is_within(5, reference)); // diff = 2
3049 assert!(Frame::new(102).is_within(5, reference)); // diff = 2
3050 assert!(Frame::new(95).is_within(5, reference)); // diff = 5 (boundary)
3051 assert!(Frame::new(105).is_within(5, reference)); // diff = 5 (boundary)
3052 }
3053
3054 #[test]
3055 fn frame_is_within_outside() {
3056 let reference = Frame::new(100);
3057 assert!(!Frame::new(94).is_within(5, reference)); // diff = 6
3058 assert!(!Frame::new(106).is_within(5, reference)); // diff = 6
3059 assert!(!Frame::new(0).is_within(5, reference)); // diff = 100
3060 }
3061
3062 #[test]
3063 fn frame_is_within_zero_window() {
3064 let reference = Frame::new(100);
3065 assert!(Frame::new(100).is_within(0, reference)); // exact match
3066 assert!(!Frame::new(99).is_within(0, reference)); // any diff is outside
3067 assert!(!Frame::new(101).is_within(0, reference));
3068 }
3069
3070 #[test]
3071 fn frame_is_within_max_window() {
3072 let reference = Frame::new(0);
3073 // With max window, everything should be within
3074 assert!(Frame::new(i32::MAX).is_within(u32::MAX, reference));
3075 assert!(Frame::new(i32::MIN).is_within(u32::MAX, reference));
3076 }
3077}
3078
3079// ###################
3080// # KANI PROOFS #
3081// ###################
3082
3083/// Kani proofs for Frame arithmetic safety (SAFE-6 from formal-spec.md).
3084///
3085/// These proofs verify:
3086/// - Frame addition does not overflow in typical usage
3087/// - Frame subtraction produces correct results
3088/// - Frame comparisons are consistent
3089/// - NULL_FRAME (-1) is handled correctly
3090///
3091/// Note: Requires Kani verifier. Install with:
3092/// cargo install --locked kani-verifier
3093/// cargo kani setup
3094///
3095/// Run proofs with:
3096/// cargo kani --tests
3097#[cfg(kani)]
3098mod kani_proofs {
3099 use super::*;
3100
3101 /// Proof: Frame::new creates valid frames for non-negative inputs.
3102 ///
3103 /// - Tier: 1 (Fast, <30s)
3104 /// - Verifies: Frame construction preserves value and validity
3105 /// - Related: proof_frame_null_consistency, proof_frame_to_option
3106 #[kani::proof]
3107 fn proof_frame_new_valid() {
3108 let value: i32 = kani::any();
3109 kani::assume(value >= 0);
3110
3111 let frame = Frame::new(value);
3112 kani::assert(
3113 frame.is_valid(),
3114 "Frame::new with non-negative should be valid",
3115 );
3116 kani::assert(
3117 !frame.is_null(),
3118 "Frame::new with non-negative should not be null",
3119 );
3120 kani::assert(
3121 frame.as_i32() == value,
3122 "Frame::as_i32 should return original value",
3123 );
3124 }
3125
3126 /// Proof: Frame::NULL is consistently null.
3127 ///
3128 /// - Tier: 1 (Fast, <30s)
3129 /// - Verifies: NULL frame identity and invariants
3130 /// - Related: proof_frame_new_valid, proof_frame_to_option
3131 #[kani::proof]
3132 fn proof_frame_null_consistency() {
3133 let null_frame = Frame::NULL;
3134 kani::assert(null_frame.is_null(), "NULL frame should be null");
3135 kani::assert(!null_frame.is_valid(), "NULL frame should not be valid");
3136 kani::assert(
3137 null_frame.as_i32() == NULL_FRAME,
3138 "NULL frame should equal NULL_FRAME constant",
3139 );
3140 }
3141
3142 /// Proof: Frame addition with small positive values is safe.
3143 ///
3144 /// This proves that for frames in typical game usage (0 to 10,000,000),
3145 /// adding small increments (0-1000) does not overflow.
3146 ///
3147 /// - Tier: 2 (Medium, 30s-2min)
3148 /// - Verifies: Frame addition overflow safety (SAFE-6)
3149 /// - Related: proof_frame_add_assign_consistent, proof_frame_sub_frames_correct
3150 #[kani::proof]
3151 fn proof_frame_add_small_safe() {
3152 let frame_val: i32 = kani::any();
3153 let increment: i32 = kani::any();
3154
3155 // Typical game usage: frames 0 to 10 million, increments 0 to 1000
3156 kani::assume(frame_val >= 0 && frame_val <= 10_000_000);
3157 kani::assume(increment >= 0 && increment <= 1000);
3158
3159 let frame = Frame::new(frame_val);
3160 let result = frame + increment;
3161
3162 kani::assert(
3163 result.as_i32() == frame_val + increment,
3164 "Frame addition should be correct",
3165 );
3166 kani::assert(
3167 result.is_valid(),
3168 "Result should be valid for typical usage",
3169 );
3170 }
3171
3172 /// Proof: Frame subtraction produces correct differences.
3173 ///
3174 /// - Tier: 2 (Medium, 30s-2min)
3175 /// - Verifies: Frame subtraction correctness
3176 /// - Related: proof_frame_add_small_safe, proof_frame_sub_assign_consistent
3177 #[kani::proof]
3178 fn proof_frame_sub_frames_correct() {
3179 let a: i32 = kani::any();
3180 let b: i32 = kani::any();
3181
3182 kani::assume(a >= 0 && a <= 1_000_000);
3183 kani::assume(b >= 0 && b <= 1_000_000);
3184
3185 let frame_a = Frame::new(a);
3186 let frame_b = Frame::new(b);
3187
3188 let diff: i32 = frame_a - frame_b;
3189 kani::assert(
3190 diff == a - b,
3191 "Frame subtraction should produce correct difference",
3192 );
3193 }
3194
3195 /// Proof: Frame ordering is consistent with i32 ordering.
3196 ///
3197 /// - Tier: 2 (Medium, 30s-2min)
3198 /// - Verifies: Frame comparison operators consistency
3199 /// - Related: proof_frame_ordering
3200 #[kani::proof]
3201 fn proof_frame_ordering_consistent() {
3202 let a: i32 = kani::any();
3203 let b: i32 = kani::any();
3204
3205 kani::assume(a >= -1 && a <= 1_000_000);
3206 kani::assume(b >= -1 && b <= 1_000_000);
3207
3208 let frame_a = Frame::new(a);
3209 let frame_b = Frame::new(b);
3210
3211 // Verify ordering consistency
3212 if a < b {
3213 kani::assert(frame_a < frame_b, "Frame < should be consistent");
3214 }
3215 if a > b {
3216 kani::assert(frame_a > frame_b, "Frame > should be consistent");
3217 }
3218 if a == b {
3219 kani::assert(frame_a == frame_b, "Frame == should be consistent");
3220 }
3221 }
3222
3223 /// Proof: Frame modulo operation is correct for queue indexing.
3224 ///
3225 /// This is critical for InputQueue circular buffer indexing (INV-5).
3226 ///
3227 /// - Tier: 2 (Medium, 30s-2min)
3228 /// - Verifies: Queue index bounds via modulo (INV-5)
3229 /// - Related: proof_queue_index_calculation, proof_head_wraparound
3230 #[kani::proof]
3231 fn proof_frame_modulo_for_queue() {
3232 let frame_val: i32 = kani::any();
3233
3234 // Valid frames for queue indexing
3235 kani::assume(frame_val >= 0 && frame_val <= 10_000_000);
3236
3237 let frame = Frame::new(frame_val);
3238 let queue_len: i32 = 128; // INPUT_QUEUE_LENGTH
3239
3240 let index = frame % queue_len;
3241
3242 kani::assert(index >= 0, "Queue index should be non-negative");
3243 kani::assert(index < queue_len, "Queue index should be within bounds");
3244 kani::assert(index == frame_val % queue_len, "Modulo should be correct");
3245 }
3246
3247 /// Proof: Frame::to_option correctly handles null and valid frames.
3248 ///
3249 /// - Tier: 1 (Fast, <30s)
3250 /// - Verifies: Frame to Option conversion correctness
3251 /// - Related: proof_frame_from_option, proof_frame_null_consistency
3252 #[kani::proof]
3253 fn proof_frame_to_option() {
3254 let frame_val: i32 = kani::any();
3255 kani::assume(frame_val >= -1 && frame_val <= 1_000_000);
3256
3257 let frame = Frame::new(frame_val);
3258 let opt = frame.to_option();
3259
3260 if frame.is_valid() {
3261 kani::assert(opt.is_some(), "Valid frame should produce Some");
3262 kani::assert(opt.unwrap() == frame, "Option should contain same frame");
3263 } else {
3264 kani::assert(opt.is_none(), "Invalid frame should produce None");
3265 }
3266 }
3267
3268 /// Proof: Frame::from_option correctly handles Some and None.
3269 ///
3270 /// - Tier: 1 (Fast, <30s)
3271 /// - Verifies: Option to Frame conversion correctness
3272 /// - Related: proof_frame_to_option, proof_frame_null_consistency
3273 #[kani::proof]
3274 fn proof_frame_from_option() {
3275 let frame_val: i32 = kani::any();
3276 kani::assume(frame_val >= 0 && frame_val <= 1_000_000);
3277
3278 let frame = Frame::new(frame_val);
3279
3280 // Test with Some
3281 let from_some = Frame::from_option(Some(frame));
3282 kani::assert(from_some == frame, "from_option(Some) should return frame");
3283
3284 // Test with None
3285 let from_none = Frame::from_option(None);
3286 kani::assert(
3287 from_none == Frame::NULL,
3288 "from_option(None) should return NULL",
3289 );
3290 }
3291
3292 /// Proof: Frame AddAssign is consistent with Add.
3293 ///
3294 /// - Tier: 2 (Medium, 30s-2min)
3295 /// - Verifies: AddAssign operator equivalence with Add
3296 /// - Related: proof_frame_add_small_safe, proof_frame_sub_assign_consistent
3297 #[kani::proof]
3298 fn proof_frame_add_assign_consistent() {
3299 let frame_val: i32 = kani::any();
3300 let increment: i32 = kani::any();
3301
3302 kani::assume(frame_val >= 0 && frame_val <= 1_000_000);
3303 kani::assume(increment >= 0 && increment <= 1000);
3304
3305 let frame1 = Frame::new(frame_val);
3306 let mut frame2 = Frame::new(frame_val);
3307
3308 let result1 = frame1 + increment;
3309 frame2 += increment;
3310
3311 kani::assert(result1 == frame2, "AddAssign should be consistent with Add");
3312 }
3313
3314 /// Proof: Frame SubAssign is consistent with Sub.
3315 ///
3316 /// - Tier: 2 (Medium, 30s-2min)
3317 /// - Verifies: SubAssign operator equivalence with Sub
3318 /// - Related: proof_frame_sub_frames_correct, proof_frame_add_assign_consistent
3319 #[kani::proof]
3320 fn proof_frame_sub_assign_consistent() {
3321 let frame_val: i32 = kani::any();
3322 let decrement: i32 = kani::any();
3323
3324 kani::assume(frame_val >= 100 && frame_val <= 1_000_000);
3325 kani::assume(decrement >= 0 && decrement <= 100);
3326
3327 let frame1 = Frame::new(frame_val);
3328 let mut frame2 = Frame::new(frame_val);
3329
3330 let result1 = frame1 - decrement;
3331 frame2 -= decrement;
3332
3333 kani::assert(result1 == frame2, "SubAssign should be consistent with Sub");
3334 }
3335
3336 /// Proof: PlayerHandle validity check is correct.
3337 ///
3338 /// - Tier: 2 (Medium, 30s-2min)
3339 /// - Verifies: PlayerHandle player vs spectator classification
3340 /// - Related: proof_player_handle_preservation, proof_player_handle_equality
3341 #[kani::proof]
3342 fn proof_player_handle_validity() {
3343 let handle_val: usize = kani::any();
3344 let num_players: usize = kani::any();
3345
3346 kani::assume(handle_val < 100);
3347 kani::assume(num_players > 0 && num_players <= 16);
3348
3349 let handle = PlayerHandle::new(handle_val);
3350
3351 let is_valid_player = handle.is_valid_player_for(num_players);
3352 let is_spectator = handle.is_spectator_for(num_players);
3353
3354 // A handle is either a valid player OR a spectator, never both
3355 kani::assert(
3356 is_valid_player != is_spectator || handle_val >= num_players,
3357 "Handle should be player XOR spectator",
3358 );
3359
3360 if handle_val < num_players {
3361 kani::assert(
3362 is_valid_player,
3363 "Handle < num_players should be valid player",
3364 );
3365 kani::assert(
3366 !is_spectator,
3367 "Handle < num_players should not be spectator",
3368 );
3369 } else {
3370 kani::assert(
3371 !is_valid_player,
3372 "Handle >= num_players should not be valid player",
3373 );
3374 kani::assert(is_spectator, "Handle >= num_players should be spectator");
3375 }
3376 }
3377}