Skip to main content

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}