Skip to main content

fortress_rollback/
error.rs

1//! Error types for Fortress Rollback.
2//!
3//! This module provides structured error types for the rollback networking library.
4//! The error types are designed to be:
5//!
6//! - **Zero-allocation on hot paths**: Errors store numeric data directly instead
7//!   of formatting strings, enabling allocation-free error construction.
8//! - **Programmatically inspectable**: Using enums and structured fields instead
9//!   of string messages allows callers to match on specific error cases.
10//! - **Self-documenting**: Each error variant and field is documented.
11//!
12//! # Error Type Design
13//!
14//! ## Dual Variant Pattern
15//!
16//! Some error variants exist in both unstructured (legacy) and structured forms:
17//!
18//! - `InvalidFrame` (unstructured) vs `InvalidFrameStructured` (structured)
19//! - `InternalError` (unstructured) vs `InternalErrorStructured` (structured)
20//!
21//! This pattern exists for backward compatibility. The unstructured variants
22//! accept string messages and are used by legacy code. The structured variants
23//! use zero-allocation enums and provide better error inspection.
24//!
25//! **Migration path:** New code should use structured variants. Unstructured
26//! variants are deprecated but retained for API stability. A future major version
27//! may remove the unstructured variants.
28//!
29//! ## Structured Error Types
30//!
31//! - [`IndexOutOfBounds`]: Index/bounds error with collection name and indices.
32//! - [`InvalidFrameReason`]: Why a frame was invalid (null, negative, out of window).
33//! - [`InternalErrorKind`]: Specific internal error types with structured context.
34//! - [`RleDecodeReason`]: Why RLE decoding failed.
35//!
36//! ## Module-Specific Error Types
37//!
38//! Other modules provide their own structured error types:
39//!
40//! - [`crate::network::compression::CompressionError`]: RLE and delta decode errors.
41//! - [`crate::network::codec::CodecError`]: Serialization/deserialization errors.
42//! - [`crate::checksum::ChecksumError`]: Checksum computation errors.
43//!
44//! # Usage Examples
45//!
46//! ## Creating structured errors (preferred)
47//!
48//! ```
49//! use fortress_rollback::{FortressError, InternalErrorKind, IndexOutOfBounds};
50//!
51//! // Create a structured index out of bounds error
52//! let error = FortressError::InternalErrorStructured {
53//!     kind: InternalErrorKind::IndexOutOfBounds(IndexOutOfBounds {
54//!         name: "inputs",
55//!         index: 10,
56//!         length: 5,
57//!     }),
58//! };
59//! ```
60//!
61//! ## Matching on error variants
62//!
63//! ```
64//! use fortress_rollback::{FortressError, InvalidFrameReason};
65//!
66//! fn handle_error(err: FortressError) {
67//!     match err {
68//!         FortressError::InvalidFrameStructured { frame, reason } => {
69//!             match reason {
70//!                 InvalidFrameReason::NullFrame => {
71//!                     println!("Frame {} is NULL", frame.as_i32());
72//!                 }
73//!                 InvalidFrameReason::OutsidePredictionWindow { max_prediction, .. } => {
74//!                     println!("Frame {} outside {} frame prediction window",
75//!                              frame.as_i32(), max_prediction);
76//!                 }
77//!                 _ => {}
78//!             }
79//!         }
80//!         _ => {}
81//!     }
82//! }
83//! ```
84
85use std::error::Error;
86use std::fmt;
87use std::fmt::Display;
88
89use crate::{Frame, PlayerHandle};
90
91// =============================================================================
92// Structured Error Types for Hot Path
93// =============================================================================
94// These types store debugging data as fields (cheap - no allocation) and format
95// lazily in Display impl (only when error is displayed - cold path).
96
97/// Represents an index out of bounds error with full context.
98///
99/// This structured type stores all debugging information without allocation,
100/// and formats the message lazily in the `Display` implementation.
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
102pub struct IndexOutOfBounds {
103    /// The name of the collection that was accessed.
104    pub name: &'static str,
105    /// The index that was attempted.
106    pub index: usize,
107    /// The length of the collection.
108    pub length: usize,
109}
110
111impl Display for IndexOutOfBounds {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        write!(
114            f,
115            "{} index {} out of bounds (length: {})",
116            self.name, self.index, self.length
117        )
118    }
119}
120
121/// Represents why a frame was invalid.
122///
123/// Using an enum instead of String allows for zero-allocation error construction
124/// on hot paths while still providing detailed error messages.
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
126pub enum InvalidFrameReason {
127    /// Frame is NULL_FRAME (-1).
128    NullFrame,
129    /// Frame is negative (other than NULL_FRAME).
130    Negative,
131    /// Frame must be non-negative.
132    MustBeNonNegative,
133    /// Frame is not in the past (must load a frame before current).
134    NotInPast {
135        /// The current frame.
136        current_frame: Frame,
137    },
138    /// Frame is outside the prediction window.
139    OutsidePredictionWindow {
140        /// The current frame.
141        current_frame: Frame,
142        /// The maximum prediction depth.
143        max_prediction: usize,
144    },
145    /// The saved state for this frame has the wrong frame number.
146    WrongSavedFrame {
147        /// The frame number in the saved state.
148        saved_frame: Frame,
149    },
150    /// Frame is not confirmed yet.
151    NotConfirmed {
152        /// The highest confirmed frame.
153        confirmed_frame: Frame,
154    },
155    /// Frame is NULL or negative (general validation).
156    NullOrNegative,
157    /// No saved state exists for this frame.
158    ///
159    /// Returned when attempting to load a game state that was never saved.
160    /// This typically indicates a programming error — [`LoadGameState`] requests
161    /// should only be issued for frames that were previously saved via
162    /// [`SaveGameState`].
163    ///
164    /// [`LoadGameState`]: crate::FortressRequest::LoadGameState
165    /// [`SaveGameState`]: crate::FortressRequest::SaveGameState
166    MissingState,
167    /// Custom reason (fallback for API compatibility).
168    Custom(&'static str),
169}
170
171impl Display for InvalidFrameReason {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        match self {
174            Self::NullFrame => write!(f, "cannot load NULL_FRAME"),
175            Self::Negative => write!(f, "frame is negative"),
176            Self::MustBeNonNegative => write!(f, "frame must be non-negative"),
177            Self::NotInPast { current_frame } => {
178                write!(
179                    f,
180                    "must load frame in the past (current: {})",
181                    current_frame
182                )
183            },
184            Self::OutsidePredictionWindow {
185                current_frame,
186                max_prediction,
187            } => {
188                write!(
189                    f,
190                    "cannot load frame outside of prediction window (current: {}, max_prediction: {})",
191                    current_frame, max_prediction
192                )
193            },
194            Self::WrongSavedFrame { saved_frame } => {
195                write!(f, "saved state has wrong frame (found: {})", saved_frame)
196            },
197            Self::NotConfirmed { confirmed_frame } => {
198                write!(
199                    f,
200                    "frame is not confirmed yet (confirmed_frame: {})",
201                    confirmed_frame
202                )
203            },
204            Self::NullOrNegative => write!(f, "frame is NULL or negative"),
205            Self::MissingState => write!(f, "no saved state exists for this frame"),
206            Self::Custom(s) => write!(f, "{}", s),
207        }
208    }
209}
210
211/// Represents why an RLE decode operation failed.
212///
213/// Using an enum instead of String allows for zero-allocation error construction
214/// on hot paths while still providing detailed error messages.
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
216pub enum RleDecodeReason {
217    /// The bitfield index was out of bounds during decode.
218    BitfieldIndexOutOfBounds,
219    /// The destination slice was out of bounds during decode.
220    DestinationSliceOutOfBounds,
221    /// The source slice was out of bounds during decode.
222    SourceSliceOutOfBounds,
223    /// The encoded data was truncated (offset exceeded buffer length).
224    TruncatedData {
225        /// The offset that was reached.
226        offset: usize,
227        /// The buffer length.
228        buffer_len: usize,
229    },
230    /// An unknown or unexpected error occurred during RLE decoding.
231    ///
232    /// This variant is used as a fallback when the underlying error cannot be
233    /// mapped to a more specific reason (e.g., when downcasting fails).
234    Unknown,
235}
236
237impl Display for RleDecodeReason {
238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239        match self {
240            Self::BitfieldIndexOutOfBounds => {
241                write!(f, "bitfield index out of bounds")
242            },
243            Self::DestinationSliceOutOfBounds => {
244                write!(f, "destination slice out of bounds")
245            },
246            Self::SourceSliceOutOfBounds => {
247                write!(f, "source slice out of bounds")
248            },
249            Self::TruncatedData { offset, buffer_len } => {
250                write!(
251                    f,
252                    "truncated data: offset {} exceeds buffer length {}",
253                    offset, buffer_len
254                )
255            },
256            Self::Unknown => {
257                write!(f, "unknown RLE decode error")
258            },
259        }
260    }
261}
262
263/// Represents why a delta decode operation failed.
264///
265/// Using an enum instead of String allows for zero-allocation error construction
266/// and programmatic error inspection.
267#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
268pub enum DeltaDecodeReason {
269    /// The reference bytes were empty.
270    EmptyReference,
271    /// The data length is not a multiple of the reference length.
272    DataLengthMismatch {
273        /// The length of the data buffer.
274        data_len: usize,
275        /// The length of the reference buffer.
276        reference_len: usize,
277    },
278    /// The reference bytes index was out of bounds.
279    ReferenceIndexOutOfBounds {
280        /// The index that was out of bounds.
281        index: usize,
282        /// The length of the reference buffer.
283        length: usize,
284    },
285    /// The data index was out of bounds.
286    DataIndexOutOfBounds {
287        /// The index that was out of bounds.
288        index: usize,
289        /// The length of the data buffer.
290        length: usize,
291    },
292    /// An unknown or unexpected error occurred during delta decoding.
293    ///
294    /// This variant is used as a fallback when the underlying error cannot be
295    /// mapped to a more specific reason (e.g., when downcasting fails).
296    Unknown,
297}
298
299impl Display for DeltaDecodeReason {
300    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301        match self {
302            Self::EmptyReference => write!(f, "reference bytes is empty"),
303            Self::DataLengthMismatch {
304                data_len,
305                reference_len,
306            } => {
307                write!(
308                    f,
309                    "data length {} is not a multiple of reference length {}",
310                    data_len, reference_len
311                )
312            },
313            Self::ReferenceIndexOutOfBounds { index, length } => {
314                write!(
315                    f,
316                    "reference bytes index {} out of bounds (length: {})",
317                    index, length
318                )
319            },
320            Self::DataIndexOutOfBounds { index, length } => {
321                write!(f, "data index {} out of bounds (length: {})", index, length)
322            },
323            Self::Unknown => {
324                write!(f, "unknown delta decode error")
325            },
326        }
327    }
328}
329
330/// Specific internal error kinds with structured data.
331///
332/// Using an enum instead of String allows for zero-allocation error construction
333/// on hot paths while preserving full debugging context.
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
335pub enum InternalErrorKind {
336    /// An index was out of bounds.
337    IndexOutOfBounds(IndexOutOfBounds),
338    /// Failed to get synchronized inputs.
339    SynchronizedInputsFailed {
340        /// The frame at which inputs were requested.
341        frame: Frame,
342    },
343    /// Player inputs vector is empty.
344    EmptyPlayerInputs,
345    /// Buffer index out of bounds (generic).
346    BufferIndexOutOfBounds,
347    /// Player handle not found when checking disconnect status.
348    DisconnectStatusNotFound {
349        /// The player handle that was not found.
350        player_handle: PlayerHandle,
351    },
352    /// Endpoint not found for a registered remote player.
353    EndpointNotFoundForRemote {
354        /// The player handle for which the endpoint was not found.
355        player_handle: PlayerHandle,
356    },
357    /// Endpoint not found for a registered spectator.
358    EndpointNotFoundForSpectator {
359        /// The player handle for which the endpoint was not found.
360        player_handle: PlayerHandle,
361    },
362    /// Connection status index out of bounds when updating local connection status.
363    ConnectionStatusIndexOutOfBounds {
364        /// The player handle that was out of bounds.
365        player_handle: PlayerHandle,
366    },
367    /// RLE decode operation failed.
368    RleDecodeError {
369        /// The specific reason for the RLE decode failure.
370        reason: RleDecodeReason,
371    },
372    /// Delta decode operation failed.
373    DeltaDecodeError {
374        /// The specific reason for the delta decode failure.
375        reason: DeltaDecodeReason,
376    },
377    /// Custom error (fallback for API compatibility).
378    Custom(&'static str),
379}
380
381impl Display for InternalErrorKind {
382    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383        match self {
384            Self::IndexOutOfBounds(iob) => write!(f, "{}", iob),
385            Self::SynchronizedInputsFailed { frame } => {
386                write!(f, "failed to get synchronized inputs for frame {}", frame)
387            },
388            Self::EmptyPlayerInputs => write!(f, "player inputs vector is empty"),
389            Self::BufferIndexOutOfBounds => write!(f, "buffer index out of bounds"),
390            Self::DisconnectStatusNotFound { player_handle } => {
391                write!(
392                    f,
393                    "disconnect status not found for player handle {}",
394                    player_handle.as_usize()
395                )
396            },
397            Self::EndpointNotFoundForRemote { player_handle } => {
398                write!(
399                    f,
400                    "endpoint not found for registered remote player {}",
401                    player_handle.as_usize()
402                )
403            },
404            Self::EndpointNotFoundForSpectator { player_handle } => {
405                write!(
406                    f,
407                    "endpoint not found for registered spectator {}",
408                    player_handle.as_usize()
409                )
410            },
411            Self::ConnectionStatusIndexOutOfBounds { player_handle } => {
412                write!(
413                    f,
414                    "connection status index out of bounds for player handle {}",
415                    player_handle.as_usize()
416                )
417            },
418            Self::RleDecodeError { reason } => {
419                write!(f, "RLE decode failed: {}", reason)
420            },
421            Self::DeltaDecodeError { reason } => {
422                write!(f, "delta decode failed: {}", reason)
423            },
424            Self::Custom(s) => write!(f, "{}", s),
425        }
426    }
427}
428
429/// Represents why a request was invalid.
430///
431/// Using an enum instead of String allows for zero-allocation error construction
432/// and programmatic error inspection.
433#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
434pub enum InvalidRequestKind {
435    // Player handle errors
436    /// The player handle is already in use.
437    PlayerHandleInUse {
438        /// The handle that is already in use.
439        handle: PlayerHandle,
440    },
441    /// The player handle does not refer to a local player.
442    NotLocalPlayer {
443        /// The handle that is not a local player.
444        handle: PlayerHandle,
445    },
446    /// The player handle does not refer to a remote player or spectator.
447    NotRemotePlayerOrSpectator {
448        /// The handle that is not a remote player or spectator.
449        handle: PlayerHandle,
450    },
451    /// Invalid handle for a local player.
452    InvalidLocalPlayerHandle {
453        /// The invalid player handle.
454        handle: PlayerHandle,
455        /// The number of players in the session.
456        num_players: usize,
457    },
458    /// Invalid handle for a remote player.
459    InvalidRemotePlayerHandle {
460        /// The invalid player handle.
461        handle: PlayerHandle,
462        /// The number of players in the session.
463        num_players: usize,
464    },
465    /// Invalid handle for a spectator.
466    InvalidSpectatorHandle {
467        /// The invalid player handle.
468        handle: PlayerHandle,
469        /// The number of players in the session.
470        num_players: usize,
471    },
472
473    // Input errors
474    /// Missing local input for one or more players.
475    MissingLocalInput,
476    /// No confirmed input available for the requested frame.
477    NoConfirmedInput {
478        /// The frame for which no confirmed input was available.
479        frame: Frame,
480    },
481
482    // Configuration errors
483    /// A configuration value is outside the allowed range.
484    ConfigValueOutOfRange {
485        /// The name of the configuration field.
486        field: &'static str,
487        /// The minimum allowed value.
488        min: u64,
489        /// The maximum allowed value.
490        max: u64,
491        /// The actual value that was provided.
492        actual: u64,
493    },
494    /// A Duration configuration value is outside the allowed range.
495    DurationConfigOutOfRange {
496        /// The name of the configuration field.
497        field: &'static str,
498        /// The minimum allowed value in milliseconds.
499        min_ms: u64,
500        /// The maximum allowed value in milliseconds.
501        max_ms: u64,
502        /// The actual value provided in milliseconds.
503        actual_ms: u64,
504    },
505    /// Frame delay exceeds the maximum allowed for the queue length.
506    FrameDelayTooLarge {
507        /// The requested delay.
508        delay: usize,
509        /// The maximum allowed delay.
510        max_delay: usize,
511    },
512    /// Input delay exceeds the maximum allowed for the given FPS.
513    InputDelayTooLarge {
514        /// The requested input delay in frames.
515        delay_frames: usize,
516        /// The frames per second (for computing actual delay in seconds).
517        fps: usize,
518        /// The maximum allowed seconds (e.g., 10 for "10 seconds").
519        max_seconds_limit: usize,
520    },
521    /// Input queue length is too small (minimum is 2).
522    QueueLengthTooSmall {
523        /// The requested length.
524        length: usize,
525    },
526    /// Event queue size is too small (minimum is 10).
527    EventQueueSizeTooSmall {
528        /// The requested size.
529        size: usize,
530    },
531
532    // Session building errors
533    /// Number of players must be greater than 0.
534    ZeroPlayers,
535    /// FPS must be greater than 0.
536    ZeroFps,
537    /// Buffer size must be greater than 0.
538    ZeroBufferSize,
539    /// Not enough players have been registered.
540    NotEnoughPlayers {
541        /// The expected number of players.
542        expected: usize,
543        /// The actual number of players registered.
544        actual: usize,
545    },
546    /// Check distance is too large for the prediction window.
547    CheckDistanceTooLarge {
548        /// The requested check distance.
549        check_dist: usize,
550        /// The maximum prediction window.
551        max_prediction: usize,
552    },
553    /// Max frames behind is invalid.
554    MaxFramesBehindInvalid {
555        /// The requested value.
556        value: usize,
557        /// The buffer size.
558        buffer_size: usize,
559    },
560    /// Catchup speed is invalid.
561    CatchupSpeedInvalid {
562        /// The requested catchup speed.
563        speed: usize,
564        /// The maximum frames behind value.
565        max_frames_behind: usize,
566    },
567
568    // Disconnect errors
569    /// Cannot disconnect: player handle is invalid.
570    DisconnectInvalidHandle {
571        /// The invalid handle.
572        handle: PlayerHandle,
573    },
574    /// Cannot disconnect a local player.
575    DisconnectLocalPlayer {
576        /// The local player handle.
577        handle: PlayerHandle,
578    },
579    /// Player is already disconnected.
580    AlreadyDisconnected {
581        /// The already disconnected handle.
582        handle: PlayerHandle,
583    },
584
585    // Protocol errors
586    /// Operation called in wrong protocol state.
587    WrongProtocolState {
588        /// The current state name.
589        current_state: &'static str,
590        /// The expected state name.
591        expected_state: &'static str,
592    },
593
594    /// Custom error (fallback for API compatibility).
595    Custom(&'static str),
596}
597
598impl Display for InvalidRequestKind {
599    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
600        match self {
601            Self::PlayerHandleInUse { handle } => {
602                write!(f, "player handle {} is already in use", handle.as_usize())
603            },
604            Self::NotLocalPlayer { handle } => {
605                write!(
606                    f,
607                    "player handle {} does not refer to a local player",
608                    handle.as_usize()
609                )
610            },
611            Self::NotRemotePlayerOrSpectator { handle } => {
612                write!(
613                    f,
614                    "player handle {} does not refer to a remote player or spectator",
615                    handle.as_usize()
616                )
617            },
618            Self::InvalidLocalPlayerHandle {
619                handle,
620                num_players,
621            } => {
622                write!(
623                    f,
624                    "invalid local player handle {}: num_players is {}",
625                    handle.as_usize(),
626                    num_players
627                )
628            },
629            Self::InvalidRemotePlayerHandle {
630                handle,
631                num_players,
632            } => {
633                write!(
634                    f,
635                    "invalid remote player handle {}: num_players is {}",
636                    handle.as_usize(),
637                    num_players
638                )
639            },
640            Self::InvalidSpectatorHandle {
641                handle,
642                num_players,
643            } => {
644                write!(
645                    f,
646                    "invalid spectator handle {}: num_players is {}",
647                    handle.as_usize(),
648                    num_players
649                )
650            },
651            Self::MissingLocalInput => write!(f, "missing local input for one or more players"),
652            Self::NoConfirmedInput { frame } => {
653                write!(f, "no confirmed input available for frame {}", frame)
654            },
655            Self::ConfigValueOutOfRange {
656                field,
657                min,
658                max,
659                actual,
660            } => {
661                write!(
662                    f,
663                    "configuration value '{}' is out of range: {} not in [{}, {}]",
664                    field, actual, min, max
665                )
666            },
667            Self::DurationConfigOutOfRange {
668                field,
669                min_ms,
670                max_ms,
671                actual_ms,
672            } => {
673                write!(
674                    f,
675                    "duration configuration '{}' is out of range: {}ms not in [{}ms, {}ms]",
676                    field, actual_ms, min_ms, max_ms
677                )
678            },
679            Self::FrameDelayTooLarge { delay, max_delay } => {
680                write!(
681                    f,
682                    "frame delay {} exceeds maximum allowed delay {}",
683                    delay, max_delay
684                )
685            },
686            Self::InputDelayTooLarge {
687                delay_frames,
688                fps,
689                max_seconds_limit,
690            } => {
691                // Defensive: if fps is zero, report infinite delay rather than panic
692                let actual_seconds = if *fps > 0 {
693                    *delay_frames as f64 / *fps as f64
694                } else {
695                    f64::INFINITY
696                };
697                write!(
698                    f,
699                    "input delay {} frames ({:.2}s) exceeds maximum allowed {}s",
700                    delay_frames, actual_seconds, max_seconds_limit
701                )
702            },
703            Self::QueueLengthTooSmall { length } => {
704                write!(
705                    f,
706                    "input queue length {} is too small (minimum is 2)",
707                    length
708                )
709            },
710            Self::EventQueueSizeTooSmall { size } => {
711                write!(f, "event queue size {} is too small (minimum is 10)", size)
712            },
713            Self::ZeroPlayers => write!(f, "number of players must be greater than 0"),
714            Self::ZeroFps => write!(f, "FPS must be greater than 0"),
715            Self::ZeroBufferSize => write!(f, "buffer size must be greater than 0"),
716            Self::NotEnoughPlayers { expected, actual } => {
717                write!(
718                    f,
719                    "not enough players registered: expected {}, got {}",
720                    expected, actual
721                )
722            },
723            Self::CheckDistanceTooLarge {
724                check_dist,
725                max_prediction,
726            } => {
727                write!(
728                    f,
729                    "check distance {} is too large for prediction window {}",
730                    check_dist, max_prediction
731                )
732            },
733            Self::MaxFramesBehindInvalid { value, buffer_size } => {
734                write!(
735                    f,
736                    "max frames behind {} is invalid for buffer size {}",
737                    value, buffer_size
738                )
739            },
740            Self::CatchupSpeedInvalid {
741                speed,
742                max_frames_behind,
743            } => {
744                write!(
745                    f,
746                    "catchup speed {} is invalid for max frames behind {}",
747                    speed, max_frames_behind
748                )
749            },
750            Self::DisconnectInvalidHandle { handle } => {
751                write!(
752                    f,
753                    "cannot disconnect: player handle {} is invalid",
754                    handle.as_usize()
755                )
756            },
757            Self::DisconnectLocalPlayer { handle } => {
758                write!(f, "cannot disconnect local player {}", handle.as_usize())
759            },
760            Self::AlreadyDisconnected { handle } => {
761                write!(f, "player {} is already disconnected", handle.as_usize())
762            },
763            Self::WrongProtocolState {
764                current_state,
765                expected_state,
766            } => {
767                write!(
768                    f,
769                    "operation called in wrong protocol state: current '{}', expected '{}'",
770                    current_state, expected_state
771                )
772            },
773            Self::Custom(s) => write!(f, "{}", s),
774        }
775    }
776}
777
778/// Represents why serialization failed.
779///
780/// Using an enum instead of String allows for zero-allocation error construction
781/// and programmatic error inspection.
782#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
783pub enum SerializationErrorKind {
784    /// Failed to create a protocol endpoint for remote players.
785    EndpointCreationFailed,
786    /// Failed to create a protocol endpoint for spectators.
787    SpectatorEndpointCreationFailed,
788    /// Custom error (fallback for API compatibility).
789    Custom(&'static str),
790}
791
792impl Display for SerializationErrorKind {
793    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
794        match self {
795            Self::EndpointCreationFailed => {
796                write!(f, "failed to create protocol endpoint for remote players")
797            },
798            Self::SpectatorEndpointCreationFailed => {
799                write!(f, "failed to create protocol endpoint for spectators")
800            },
801            Self::Custom(s) => write!(f, "{}", s),
802        }
803    }
804}
805
806/// Represents why a socket operation failed.
807///
808/// Using an enum instead of String allows for zero-allocation error construction
809/// and programmatic error inspection.
810#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
811pub enum SocketErrorKind {
812    /// Failed to bind socket to the specified port.
813    BindFailed {
814        /// The port that failed to bind.
815        port: u16,
816    },
817    /// Failed to bind after multiple retry attempts.
818    BindFailedAfterRetries {
819        /// The port that failed to bind.
820        port: u16,
821        /// The number of attempts made.
822        attempts: u8,
823    },
824    /// Custom error (fallback for API compatibility).
825    Custom(&'static str),
826}
827
828impl Display for SocketErrorKind {
829    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
830        match self {
831            Self::BindFailed { port } => {
832                write!(f, "failed to bind socket to port {}", port)
833            },
834            Self::BindFailedAfterRetries { port, attempts } => {
835                write!(
836                    f,
837                    "failed to bind socket to port {} after {} attempts",
838                    port, attempts
839                )
840            },
841            Self::Custom(s) => write!(f, "{}", s),
842        }
843    }
844}
845
846// =============================================================================
847// Main Error Enum
848// =============================================================================
849
850/// This enum contains all error messages this library can return. Most API functions will generally return a [`Result<(), FortressError>`].
851///
852/// # Error Handling
853///
854/// Match on specific error variants to handle each case:
855///
856/// ```ignore
857/// match error {
858///     FortressError::NotSynchronized => { /* handle */ }
859///     FortressError::PredictionThreshold => { /* handle */ }
860///     // ... handle all other variants
861/// }
862/// ```
863///
864/// [`Result<(), FortressError>`]: std::result::Result
865#[derive(Debug, Clone, PartialEq, Eq, Hash)]
866pub enum FortressError {
867    /// When the prediction threshold has been reached, we cannot accept more inputs from the local player.
868    PredictionThreshold,
869    /// You made an invalid request, usually by using wrong parameters for function calls.
870    ///
871    /// **Note**: For new code, prefer using [`FortressError::InvalidRequestStructured`] which
872    /// provides zero-allocation error construction and programmatic error inspection.
873    InvalidRequest {
874        /// Further specifies why the request was invalid.
875        info: String,
876    },
877    /// In a [`SyncTestSession`], this error is returned if checksums of resimulated frames do not match up with the original checksum.
878    ///
879    /// [`SyncTestSession`]: crate::SyncTestSession
880    MismatchedChecksum {
881        /// The frame at which the mismatch occurred.
882        current_frame: Frame,
883        /// The frames with mismatched checksums (one or more)
884        mismatched_frames: Vec<Frame>,
885    },
886    /// The Session is not synchronized yet. Please start the session and wait a few ms to let the clients synchronize.
887    NotSynchronized,
888    /// The spectator got so far behind the host that catching up is impossible.
889    SpectatorTooFarBehind,
890    /// An invalid frame number was provided. Frames must be non-negative and within valid ranges.
891    InvalidFrame {
892        /// The frame that was invalid.
893        frame: Frame,
894        /// A description of why the frame was invalid (legacy String variant).
895        reason: String,
896    },
897    /// An invalid frame number was provided, with structured reason (zero-allocation on hot path).
898    ///
899    /// This variant is preferred over `InvalidFrame` on hot paths as it avoids
900    /// allocating a String for the reason.
901    InvalidFrameStructured {
902        /// The frame that was invalid.
903        frame: Frame,
904        /// The structured reason why the frame was invalid.
905        reason: InvalidFrameReason,
906    },
907    /// An invalid player handle was provided. Player handles must be less than the number of players.
908    InvalidPlayerHandle {
909        /// The player handle that was invalid.
910        handle: PlayerHandle,
911        /// The maximum valid player handle (num_players - 1).
912        max_handle: PlayerHandle,
913    },
914    /// A required input was missing for the specified frame.
915    MissingInput {
916        /// The player handle whose input was missing.
917        player_handle: PlayerHandle,
918        /// The frame for which input was missing.
919        frame: Frame,
920    },
921    /// Serialization or deserialization of data failed.
922    ///
923    /// **Note**: For new code, prefer using [`FortressError::SerializationErrorStructured`] which
924    /// provides zero-allocation error construction and programmatic error inspection.
925    SerializationError {
926        /// A description of what failed to serialize/deserialize.
927        context: String,
928    },
929    /// An internal error occurred that should not happen under normal operation.
930    /// If you encounter this error, please report it as a bug.
931    InternalError {
932        /// A description of the internal error.
933        context: String,
934    },
935    /// An internal error with structured data (zero-allocation on hot path).
936    ///
937    /// This variant is preferred over `InternalError` on hot paths as it avoids
938    /// allocating a String for the context.
939    InternalErrorStructured {
940        /// The structured kind of internal error.
941        kind: InternalErrorKind,
942    },
943    /// A network socket operation failed.
944    ///
945    /// **Note**: For new code, prefer using [`FortressError::SocketErrorStructured`] which
946    /// provides zero-allocation error construction and programmatic error inspection.
947    SocketError {
948        /// A description of the socket error.
949        context: String,
950    },
951    /// An invalid request with structured reason (zero-allocation on hot path).
952    ///
953    /// This variant is preferred over `InvalidRequest` as it avoids
954    /// allocating a String for the info.
955    InvalidRequestStructured {
956        /// The structured kind of invalid request.
957        kind: InvalidRequestKind,
958    },
959    /// Serialization error with structured reason (zero-allocation on hot path).
960    ///
961    /// This variant is preferred over `SerializationError` as it avoids
962    /// allocating a String for the context.
963    SerializationErrorStructured {
964        /// The structured kind of serialization error.
965        kind: SerializationErrorKind,
966    },
967    /// Socket error with structured reason (zero-allocation on hot path).
968    ///
969    /// This variant is preferred over `SocketError` as it avoids
970    /// allocating a String for the context.
971    SocketErrorStructured {
972        /// The structured kind of socket error.
973        kind: SocketErrorKind,
974    },
975    /// Frame arithmetic operation would overflow.
976    ///
977    /// This error is returned when a frame arithmetic operation (e.g., `try_add`,
978    /// `try_sub`) would result in a value that cannot be represented as an `i32`.
979    FrameArithmeticOverflow {
980        /// The frame the operation was attempted on.
981        frame: Frame,
982        /// The operand that caused overflow.
983        operand: i32,
984        /// The operation that was attempted (e.g., "add", "sub").
985        operation: &'static str,
986    },
987    /// Frame value is too large to represent.
988    ///
989    /// This error is returned when converting a `usize` to a `Frame` and the
990    /// value exceeds `i32::MAX`.
991    FrameValueTooLarge {
992        /// The value that was too large.
993        value: usize,
994    },
995}
996
997impl Display for FortressError {
998    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
999        match self {
1000            Self::PredictionThreshold => {
1001                write!(
1002                    f,
1003                    "Prediction threshold is reached, cannot proceed without catching up."
1004                )
1005            },
1006            Self::InvalidRequest { info } => {
1007                write!(f, "Invalid Request: {}", info)
1008            },
1009            Self::NotSynchronized => {
1010                write!(
1011                    f,
1012                    "The session is not yet synchronized with all remote sessions."
1013                )
1014            },
1015            Self::MismatchedChecksum {
1016                current_frame,
1017                mismatched_frames,
1018            } => {
1019                write!(
1020                    f,
1021                    "Detected checksum mismatch during rollback on frame {}, mismatched frames: {:?}",
1022                    current_frame, mismatched_frames
1023                )
1024            },
1025            Self::SpectatorTooFarBehind => {
1026                write!(
1027                    f,
1028                    "The spectator got so far behind the host that catching up is impossible."
1029                )
1030            },
1031            Self::InvalidFrame { frame, reason } => {
1032                write!(f, "Invalid frame {}: {}", frame, reason)
1033            },
1034            Self::InvalidFrameStructured { frame, reason } => {
1035                write!(f, "Invalid frame {}: {}", frame, reason)
1036            },
1037            Self::InvalidPlayerHandle { handle, max_handle } => {
1038                write!(
1039                    f,
1040                    "Invalid player handle {}: must be less than or equal to {}",
1041                    handle, max_handle
1042                )
1043            },
1044            Self::MissingInput {
1045                player_handle,
1046                frame,
1047            } => {
1048                write!(
1049                    f,
1050                    "Missing input for player {} at frame {}",
1051                    player_handle.as_usize(),
1052                    frame
1053                )
1054            },
1055            Self::SerializationError { context } => {
1056                write!(f, "Serialization error: {}", context)
1057            },
1058            Self::InternalError { context } => {
1059                write!(f, "Internal error (please report as bug): {}", context)
1060            },
1061            Self::InternalErrorStructured { kind } => {
1062                write!(f, "Internal error (please report as bug): {}", kind)
1063            },
1064            Self::SocketError { context } => {
1065                write!(f, "Socket error: {}", context)
1066            },
1067            Self::InvalidRequestStructured { kind } => {
1068                write!(f, "Invalid Request: {}", kind)
1069            },
1070            Self::SerializationErrorStructured { kind } => {
1071                write!(f, "Serialization error: {}", kind)
1072            },
1073            Self::SocketErrorStructured { kind } => {
1074                write!(f, "Socket error: {}", kind)
1075            },
1076            Self::FrameArithmeticOverflow {
1077                frame,
1078                operand,
1079                operation,
1080            } => {
1081                write!(
1082                    f,
1083                    "Frame arithmetic overflow: {} {} on frame {}",
1084                    operation, operand, frame
1085                )
1086            },
1087            Self::FrameValueTooLarge { value } => {
1088                write!(
1089                    f,
1090                    "Frame value too large: {} exceeds i32::MAX ({})",
1091                    value,
1092                    i32::MAX
1093                )
1094            },
1095        }
1096    }
1097}
1098
1099impl Error for FortressError {
1100    /// Returns the lower-level source of this error, if any.
1101    ///
1102    /// Currently, `FortressError` variants store error context as strings rather than
1103    /// wrapping underlying error types. This design choice:
1104    /// - Keeps the error type `Clone` and `PartialEq` (which `dyn Error` cannot be)
1105    /// - Avoids complexity in serializing errors across network boundaries
1106    /// - Maintains a simple, stable API
1107    ///
1108    /// If you need to preserve the original error, consider logging it before
1109    /// converting to `FortressError`.
1110    fn source(&self) -> Option<&(dyn Error + 'static)> {
1111        // Error context is stored as strings, not wrapped errors.
1112        // This is intentional - see documentation above.
1113        None
1114    }
1115}
1116
1117impl From<InvalidRequestKind> for FortressError {
1118    fn from(kind: InvalidRequestKind) -> Self {
1119        Self::InvalidRequestStructured { kind }
1120    }
1121}
1122
1123impl From<SerializationErrorKind> for FortressError {
1124    fn from(kind: SerializationErrorKind) -> Self {
1125        Self::SerializationErrorStructured { kind }
1126    }
1127}
1128
1129impl From<SocketErrorKind> for FortressError {
1130    fn from(kind: SocketErrorKind) -> Self {
1131        Self::SocketErrorStructured { kind }
1132    }
1133}
1134
1135#[cfg(test)]
1136#[allow(
1137    clippy::panic,
1138    clippy::unwrap_used,
1139    clippy::expect_used,
1140    clippy::indexing_slicing
1141)]
1142mod tests {
1143    use super::*;
1144
1145    #[test]
1146    fn test_prediction_threshold_display() {
1147        let err = FortressError::PredictionThreshold;
1148        let display = format!("{}", err);
1149        assert!(display.contains("Prediction threshold"));
1150        assert!(display.contains("cannot proceed"));
1151    }
1152
1153    #[test]
1154    fn test_invalid_request_display() {
1155        let err = FortressError::InvalidRequest {
1156            info: "test error info".to_string(),
1157        };
1158        let display = format!("{}", err);
1159        assert!(display.contains("Invalid Request"));
1160        assert!(display.contains("test error info"));
1161    }
1162
1163    #[test]
1164    fn test_not_synchronized_display() {
1165        let err = FortressError::NotSynchronized;
1166        let display = format!("{}", err);
1167        assert!(display.contains("not yet synchronized"));
1168    }
1169
1170    #[test]
1171    fn test_mismatched_checksum_display() {
1172        let err = FortressError::MismatchedChecksum {
1173            current_frame: Frame::new(100),
1174            mismatched_frames: vec![Frame::new(95), Frame::new(96)],
1175        };
1176        let display = format!("{}", err);
1177        assert!(display.contains("checksum mismatch"));
1178        assert!(display.contains("100"));
1179    }
1180
1181    #[test]
1182    fn test_spectator_too_far_behind_display() {
1183        let err = FortressError::SpectatorTooFarBehind;
1184        let display = format!("{}", err);
1185        assert!(display.contains("spectator"));
1186        assert!(display.contains("behind"));
1187    }
1188
1189    #[test]
1190    fn test_invalid_frame_display() {
1191        let err = FortressError::InvalidFrame {
1192            frame: Frame::new(42),
1193            reason: "frame is in the past".to_string(),
1194        };
1195        let display = format!("{}", err);
1196        assert!(display.contains("Invalid frame"));
1197        assert!(display.contains("42"));
1198        assert!(display.contains("frame is in the past"));
1199    }
1200
1201    #[test]
1202    fn test_invalid_player_handle_display() {
1203        let err = FortressError::InvalidPlayerHandle {
1204            handle: PlayerHandle(5),
1205            max_handle: PlayerHandle(3),
1206        };
1207        let display = format!("{}", err);
1208        assert!(display.contains("Invalid player handle"));
1209        assert!(display.contains('5'));
1210        assert!(display.contains('3'));
1211    }
1212
1213    #[test]
1214    fn test_missing_input_display() {
1215        let err = FortressError::MissingInput {
1216            player_handle: PlayerHandle(1),
1217            frame: Frame::new(50),
1218        };
1219        let display = format!("{}", err);
1220        assert!(display.contains("Missing input"));
1221        assert!(display.contains("player 1"));
1222        assert!(display.contains("frame 50"));
1223    }
1224
1225    #[test]
1226    fn test_serialization_error_display() {
1227        let err = FortressError::SerializationError {
1228            context: "failed to serialize game state".to_string(),
1229        };
1230        let display = format!("{}", err);
1231        assert!(display.contains("Serialization error"));
1232        assert!(display.contains("failed to serialize game state"));
1233    }
1234
1235    #[test]
1236    fn test_internal_error_display() {
1237        let err = FortressError::InternalError {
1238            context: "unexpected state transition".to_string(),
1239        };
1240        let display = format!("{}", err);
1241        assert!(display.contains("Internal error"));
1242        assert!(display.contains("please report as bug"));
1243        assert!(display.contains("unexpected state transition"));
1244    }
1245
1246    #[test]
1247    fn test_socket_error_display() {
1248        let err = FortressError::SocketError {
1249            context: "connection refused".to_string(),
1250        };
1251        let display = format!("{}", err);
1252        assert!(display.contains("Socket error"));
1253        assert!(display.contains("connection refused"));
1254    }
1255
1256    #[test]
1257    fn test_error_debug() {
1258        let err = FortressError::PredictionThreshold;
1259        let debug = format!("{:?}", err);
1260        assert!(debug.contains("PredictionThreshold"));
1261    }
1262
1263    #[test]
1264    #[allow(clippy::redundant_clone)] // Testing Clone trait implementation
1265    fn test_error_clone() {
1266        let err = FortressError::InvalidRequest {
1267            info: "test".to_string(),
1268        };
1269        let cloned = err.clone();
1270        assert_eq!(err, cloned);
1271    }
1272
1273    #[test]
1274    fn test_error_partial_eq() {
1275        let err1 = FortressError::NotSynchronized;
1276        let err2 = FortressError::NotSynchronized;
1277        let err3 = FortressError::PredictionThreshold;
1278        assert_eq!(err1, err2);
1279        assert_ne!(err1, err3);
1280    }
1281
1282    #[test]
1283    fn test_error_implements_std_error() {
1284        let err: Box<dyn Error> = Box::new(FortressError::NotSynchronized);
1285        // This test verifies that FortressError implements std::error::Error
1286        assert!(err.source().is_none());
1287    }
1288
1289    // =========================================================================
1290    // Structured Error Type Tests
1291    // =========================================================================
1292
1293    #[test]
1294    fn test_index_out_of_bounds_display() {
1295        let err = IndexOutOfBounds {
1296            name: "input_queues",
1297            index: 5,
1298            length: 3,
1299        };
1300        let display = format!("{}", err);
1301        assert!(display.contains("input_queues"));
1302        assert!(display.contains('5'));
1303        assert!(display.contains('3'));
1304        assert!(display.contains("out of bounds"));
1305    }
1306
1307    #[test]
1308    fn test_invalid_frame_reason_null_frame() {
1309        let reason = InvalidFrameReason::NullFrame;
1310        let display = format!("{}", reason);
1311        assert!(display.contains("NULL_FRAME"));
1312    }
1313
1314    #[test]
1315    fn test_invalid_frame_reason_not_in_past() {
1316        let reason = InvalidFrameReason::NotInPast {
1317            current_frame: Frame::new(10),
1318        };
1319        let display = format!("{}", reason);
1320        assert!(display.contains("past"));
1321        assert!(display.contains("10"));
1322    }
1323
1324    #[test]
1325    fn test_invalid_frame_reason_outside_prediction_window() {
1326        let reason = InvalidFrameReason::OutsidePredictionWindow {
1327            current_frame: Frame::new(100),
1328            max_prediction: 8,
1329        };
1330        let display = format!("{}", reason);
1331        assert!(display.contains("prediction window"));
1332        assert!(display.contains("100"));
1333        assert!(display.contains('8'));
1334    }
1335
1336    #[test]
1337    fn test_invalid_frame_reason_wrong_saved_frame() {
1338        let reason = InvalidFrameReason::WrongSavedFrame {
1339            saved_frame: Frame::new(42),
1340        };
1341        let display = format!("{}", reason);
1342        assert!(display.contains("wrong frame"));
1343        assert!(display.contains("42"));
1344    }
1345
1346    #[test]
1347    fn test_invalid_frame_reason_not_confirmed() {
1348        let reason = InvalidFrameReason::NotConfirmed {
1349            confirmed_frame: Frame::new(50),
1350        };
1351        let display = format!("{}", reason);
1352        assert!(display.contains("not confirmed"));
1353        assert!(display.contains("50"));
1354    }
1355
1356    #[test]
1357    fn invalid_frame_reason_missing_state_display() {
1358        let reason = InvalidFrameReason::MissingState;
1359        let display = format!("{reason}");
1360        assert!(
1361            display.contains("saved state"),
1362            "Expected 'saved state' in: {display}"
1363        );
1364    }
1365
1366    #[test]
1367    fn test_internal_error_kind_index_out_of_bounds() {
1368        let kind = InternalErrorKind::IndexOutOfBounds(IndexOutOfBounds {
1369            name: "states",
1370            index: 10,
1371            length: 5,
1372        });
1373        let display = format!("{}", kind);
1374        assert!(display.contains("states"));
1375        assert!(display.contains("10"));
1376        assert!(display.contains('5'));
1377    }
1378
1379    #[test]
1380    fn test_internal_error_kind_synchronized_inputs_failed() {
1381        let kind = InternalErrorKind::SynchronizedInputsFailed {
1382            frame: Frame::new(25),
1383        };
1384        let display = format!("{}", kind);
1385        assert!(display.contains("synchronized inputs"));
1386        assert!(display.contains("25"));
1387    }
1388
1389    #[test]
1390    fn test_internal_error_kind_empty_player_inputs() {
1391        let kind = InternalErrorKind::EmptyPlayerInputs;
1392        let display = format!("{}", kind);
1393        assert!(display.contains("empty"));
1394    }
1395
1396    #[test]
1397    fn test_invalid_frame_structured_display() {
1398        let err = FortressError::InvalidFrameStructured {
1399            frame: Frame::new(42),
1400            reason: InvalidFrameReason::NullFrame,
1401        };
1402        let display = format!("{}", err);
1403        assert!(display.contains("Invalid frame"));
1404        assert!(display.contains("42"));
1405        assert!(display.contains("NULL_FRAME"));
1406    }
1407
1408    #[test]
1409    fn test_internal_error_structured_display() {
1410        let err = FortressError::InternalErrorStructured {
1411            kind: InternalErrorKind::BufferIndexOutOfBounds,
1412        };
1413        let display = format!("{}", err);
1414        assert!(display.contains("Internal error"));
1415        assert!(display.contains("buffer index out of bounds"));
1416    }
1417
1418    #[test]
1419    fn test_internal_error_kind_disconnect_status_not_found() {
1420        let kind = InternalErrorKind::DisconnectStatusNotFound {
1421            player_handle: PlayerHandle(3),
1422        };
1423        let display = format!("{}", kind);
1424        assert!(display.contains("disconnect status"));
1425        assert!(display.contains("player handle 3"));
1426    }
1427
1428    #[test]
1429    fn test_internal_error_kind_endpoint_not_found_for_remote() {
1430        let kind = InternalErrorKind::EndpointNotFoundForRemote {
1431            player_handle: PlayerHandle(5),
1432        };
1433        let display = format!("{}", kind);
1434        assert!(display.contains("endpoint not found"));
1435        assert!(display.contains("remote player 5"));
1436    }
1437
1438    #[test]
1439    fn test_internal_error_kind_endpoint_not_found_for_spectator() {
1440        let kind = InternalErrorKind::EndpointNotFoundForSpectator {
1441            player_handle: PlayerHandle(7),
1442        };
1443        let display = format!("{}", kind);
1444        assert!(display.contains("endpoint not found"));
1445        assert!(display.contains("spectator 7"));
1446    }
1447
1448    #[test]
1449    fn test_structured_errors_are_copy() {
1450        // Verify that structured error types are Copy (important for hot path)
1451        let iob = IndexOutOfBounds {
1452            name: "test",
1453            index: 1,
1454            length: 2,
1455        };
1456        let iob2 = iob; // Copy
1457        assert_eq!(iob, iob2);
1458
1459        let reason = InvalidFrameReason::NullFrame;
1460        let reason2 = reason; // Copy
1461        assert_eq!(reason, reason2);
1462
1463        let kind = InternalErrorKind::EmptyPlayerInputs;
1464        let kind2 = kind; // Copy
1465        assert_eq!(kind, kind2);
1466    }
1467
1468    // =========================================================================
1469    // RLE Decode Reason Tests
1470    // =========================================================================
1471
1472    #[test]
1473    fn test_rle_decode_reason_bitfield_index_out_of_bounds() {
1474        let reason = RleDecodeReason::BitfieldIndexOutOfBounds;
1475        let display = format!("{}", reason);
1476        assert!(display.contains("bitfield index out of bounds"));
1477    }
1478
1479    #[test]
1480    fn test_rle_decode_reason_destination_slice_out_of_bounds() {
1481        let reason = RleDecodeReason::DestinationSliceOutOfBounds;
1482        let display = format!("{}", reason);
1483        assert!(display.contains("destination slice out of bounds"));
1484    }
1485
1486    #[test]
1487    fn test_rle_decode_reason_source_slice_out_of_bounds() {
1488        let reason = RleDecodeReason::SourceSliceOutOfBounds;
1489        let display = format!("{}", reason);
1490        assert!(display.contains("source slice out of bounds"));
1491    }
1492
1493    #[test]
1494    fn test_rle_decode_reason_truncated_data() {
1495        let reason = RleDecodeReason::TruncatedData {
1496            offset: 100,
1497            buffer_len: 50,
1498        };
1499        let display = format!("{}", reason);
1500        assert!(display.contains("truncated data"));
1501        assert!(display.contains("100"));
1502        assert!(display.contains("50"));
1503    }
1504
1505    #[test]
1506    fn test_rle_decode_reason_unknown() {
1507        let reason = RleDecodeReason::Unknown;
1508        let display = format!("{}", reason);
1509        assert!(display.contains("unknown RLE decode error"));
1510    }
1511
1512    #[test]
1513    fn test_internal_error_kind_rle_decode_error() {
1514        let kind = InternalErrorKind::RleDecodeError {
1515            reason: RleDecodeReason::BitfieldIndexOutOfBounds,
1516        };
1517        let display = format!("{}", kind);
1518        assert!(display.contains("RLE decode failed"));
1519        assert!(display.contains("bitfield index out of bounds"));
1520    }
1521
1522    #[test]
1523    fn test_rle_decode_reason_is_copy() {
1524        // Verify RleDecodeReason is Copy (important for hot path)
1525        let reason = RleDecodeReason::BitfieldIndexOutOfBounds;
1526        let reason2 = reason; // Copy
1527        assert_eq!(reason, reason2);
1528
1529        let reason_with_data = RleDecodeReason::TruncatedData {
1530            offset: 10,
1531            buffer_len: 5,
1532        };
1533        let reason_with_data2 = reason_with_data; // Copy
1534        assert_eq!(reason_with_data, reason_with_data2);
1535
1536        let reason_unknown = RleDecodeReason::Unknown;
1537        let reason_unknown2 = reason_unknown; // Copy
1538        assert_eq!(reason_unknown, reason_unknown2);
1539    }
1540
1541    // =========================================================================
1542    // InvalidRequestKind Tests
1543    // =========================================================================
1544
1545    #[test]
1546    fn test_invalid_request_kind_player_handle_in_use() {
1547        let kind = InvalidRequestKind::PlayerHandleInUse {
1548            handle: PlayerHandle(3),
1549        };
1550        let display = format!("{}", kind);
1551        assert!(display.contains("player handle 3"));
1552        assert!(display.contains("already in use"));
1553    }
1554
1555    #[test]
1556    fn test_invalid_request_kind_not_local_player() {
1557        let kind = InvalidRequestKind::NotLocalPlayer {
1558            handle: PlayerHandle(2),
1559        };
1560        let display = format!("{}", kind);
1561        assert!(display.contains("player handle 2"));
1562        assert!(display.contains("local player"));
1563    }
1564
1565    #[test]
1566    fn test_invalid_request_kind_not_remote_player_or_spectator() {
1567        let kind = InvalidRequestKind::NotRemotePlayerOrSpectator {
1568            handle: PlayerHandle(1),
1569        };
1570        let display = format!("{}", kind);
1571        assert!(display.contains("player handle 1"));
1572        assert!(display.contains("remote player or spectator"));
1573    }
1574
1575    #[test]
1576    fn test_invalid_request_kind_missing_local_input() {
1577        let kind = InvalidRequestKind::MissingLocalInput;
1578        let display = format!("{}", kind);
1579        assert!(display.contains("missing local input"));
1580    }
1581
1582    #[test]
1583    fn test_invalid_request_kind_no_confirmed_input() {
1584        let kind = InvalidRequestKind::NoConfirmedInput {
1585            frame: Frame::new(42),
1586        };
1587        let display = format!("{}", kind);
1588        assert!(display.contains("no confirmed input"));
1589        assert!(display.contains("42"));
1590    }
1591
1592    #[test]
1593    fn test_invalid_request_kind_config_value_out_of_range() {
1594        let kind = InvalidRequestKind::ConfigValueOutOfRange {
1595            field: "fps",
1596            min: 1,
1597            max: 120,
1598            actual: 0,
1599        };
1600        let display = format!("{}", kind);
1601        assert!(display.contains("fps"));
1602        assert!(display.contains("out of range"));
1603        assert!(display.contains('0'));
1604        assert!(display.contains('1'));
1605        assert!(display.contains("120"));
1606    }
1607
1608    #[test]
1609    fn test_invalid_request_kind_frame_delay_too_large() {
1610        let kind = InvalidRequestKind::FrameDelayTooLarge {
1611            delay: 10,
1612            max_delay: 5,
1613        };
1614        let display = format!("{}", kind);
1615        assert!(display.contains("frame delay"));
1616        assert!(display.contains("10"));
1617        assert!(display.contains('5'));
1618    }
1619
1620    #[test]
1621    fn test_invalid_request_kind_queue_length_too_small() {
1622        let kind = InvalidRequestKind::QueueLengthTooSmall { length: 1 };
1623        let display = format!("{}", kind);
1624        assert!(display.contains("queue length"));
1625        assert!(display.contains('1'));
1626        assert!(display.contains("minimum is 2"));
1627    }
1628
1629    #[test]
1630    fn test_invalid_request_kind_event_queue_size_too_small() {
1631        let kind = InvalidRequestKind::EventQueueSizeTooSmall { size: 5 };
1632        let display = format!("{}", kind);
1633        assert!(display.contains("event queue size"));
1634        assert!(display.contains('5'));
1635        assert!(display.contains("minimum is 10"));
1636    }
1637
1638    #[test]
1639    fn test_invalid_request_kind_zero_players() {
1640        let kind = InvalidRequestKind::ZeroPlayers;
1641        let display = format!("{}", kind);
1642        assert!(display.contains("players"));
1643        assert!(display.contains("greater than 0"));
1644    }
1645
1646    #[test]
1647    fn test_invalid_request_kind_zero_fps() {
1648        let kind = InvalidRequestKind::ZeroFps;
1649        let display = format!("{}", kind);
1650        assert!(display.contains("FPS"));
1651        assert!(display.contains("greater than 0"));
1652    }
1653
1654    #[test]
1655    fn test_invalid_request_kind_zero_buffer_size() {
1656        let kind = InvalidRequestKind::ZeroBufferSize;
1657        let display = format!("{}", kind);
1658        assert!(display.contains("buffer size"));
1659        assert!(display.contains("greater than 0"));
1660    }
1661
1662    #[test]
1663    fn test_invalid_request_kind_not_enough_players() {
1664        let kind = InvalidRequestKind::NotEnoughPlayers {
1665            expected: 4,
1666            actual: 2,
1667        };
1668        let display = format!("{}", kind);
1669        assert!(display.contains("not enough players"));
1670        assert!(display.contains('4'));
1671        assert!(display.contains('2'));
1672    }
1673
1674    #[test]
1675    fn test_invalid_request_kind_check_distance_too_large() {
1676        let kind = InvalidRequestKind::CheckDistanceTooLarge {
1677            check_dist: 20,
1678            max_prediction: 10,
1679        };
1680        let display = format!("{}", kind);
1681        assert!(display.contains("check distance"));
1682        assert!(display.contains("20"));
1683        assert!(display.contains("10"));
1684    }
1685
1686    #[test]
1687    fn test_invalid_request_kind_max_frames_behind_invalid() {
1688        let kind = InvalidRequestKind::MaxFramesBehindInvalid {
1689            value: 100,
1690            buffer_size: 50,
1691        };
1692        let display = format!("{}", kind);
1693        assert!(display.contains("max frames behind"));
1694        assert!(display.contains("100"));
1695        assert!(display.contains("50"));
1696    }
1697
1698    #[test]
1699    fn test_invalid_request_kind_catchup_speed_invalid() {
1700        let kind = InvalidRequestKind::CatchupSpeedInvalid {
1701            speed: 5,
1702            max_frames_behind: 2,
1703        };
1704        let display = format!("{}", kind);
1705        assert!(display.contains("catchup speed"));
1706        assert!(display.contains('5'));
1707        assert!(display.contains('2'));
1708    }
1709
1710    #[test]
1711    fn test_invalid_request_kind_disconnect_invalid_handle() {
1712        let kind = InvalidRequestKind::DisconnectInvalidHandle {
1713            handle: PlayerHandle(99),
1714        };
1715        let display = format!("{}", kind);
1716        assert!(display.contains("disconnect"));
1717        assert!(display.contains("99"));
1718        assert!(display.contains("invalid"));
1719    }
1720
1721    #[test]
1722    fn test_invalid_request_kind_disconnect_local_player() {
1723        let kind = InvalidRequestKind::DisconnectLocalPlayer {
1724            handle: PlayerHandle(0),
1725        };
1726        let display = format!("{}", kind);
1727        assert!(display.contains("disconnect"));
1728        assert!(display.contains("local player"));
1729        assert!(display.contains('0'));
1730    }
1731
1732    #[test]
1733    fn test_invalid_request_kind_already_disconnected() {
1734        let kind = InvalidRequestKind::AlreadyDisconnected {
1735            handle: PlayerHandle(2),
1736        };
1737        let display = format!("{}", kind);
1738        assert!(display.contains("already disconnected"));
1739        assert!(display.contains('2'));
1740    }
1741
1742    #[test]
1743    fn test_invalid_request_kind_wrong_protocol_state() {
1744        let kind = InvalidRequestKind::WrongProtocolState {
1745            current_state: "Running",
1746            expected_state: "Synchronizing",
1747        };
1748        let display = format!("{}", kind);
1749        assert!(display.contains("wrong protocol state"));
1750        assert!(display.contains("Running"));
1751        assert!(display.contains("Synchronizing"));
1752    }
1753
1754    #[test]
1755    fn test_invalid_request_kind_custom() {
1756        let kind = InvalidRequestKind::Custom("custom error message");
1757        let display = format!("{}", kind);
1758        assert!(display.contains("custom error message"));
1759    }
1760
1761    #[test]
1762    fn test_invalid_request_kind_is_copy() {
1763        // Verify InvalidRequestKind is Copy (important for hot path)
1764        let kind = InvalidRequestKind::ZeroPlayers;
1765        let kind2 = kind; // Copy
1766        assert_eq!(kind, kind2);
1767
1768        let kind_with_data = InvalidRequestKind::PlayerHandleInUse {
1769            handle: PlayerHandle(1),
1770        };
1771        let kind_with_data2 = kind_with_data; // Copy
1772        assert_eq!(kind_with_data, kind_with_data2);
1773    }
1774
1775    // =========================================================================
1776    // SerializationErrorKind Tests
1777    // =========================================================================
1778
1779    #[test]
1780    fn test_serialization_error_kind_endpoint_creation_failed() {
1781        let kind = SerializationErrorKind::EndpointCreationFailed;
1782        let display = format!("{}", kind);
1783        assert!(display.contains("failed to create"));
1784        assert!(display.contains("endpoint"));
1785        assert!(display.contains("remote players"));
1786    }
1787
1788    #[test]
1789    fn test_serialization_error_kind_spectator_endpoint_creation_failed() {
1790        let kind = SerializationErrorKind::SpectatorEndpointCreationFailed;
1791        let display = format!("{}", kind);
1792        assert!(display.contains("failed to create"));
1793        assert!(display.contains("endpoint"));
1794        assert!(display.contains("spectators"));
1795    }
1796
1797    #[test]
1798    fn test_serialization_error_kind_custom() {
1799        let kind = SerializationErrorKind::Custom("custom serialization error");
1800        let display = format!("{}", kind);
1801        assert!(display.contains("custom serialization error"));
1802    }
1803
1804    #[test]
1805    fn test_serialization_error_kind_is_copy() {
1806        // Verify SerializationErrorKind is Copy (important for hot path)
1807        let kind = SerializationErrorKind::EndpointCreationFailed;
1808        let kind2 = kind; // Copy
1809        assert_eq!(kind, kind2);
1810    }
1811
1812    // =========================================================================
1813    // SocketErrorKind Tests
1814    // =========================================================================
1815
1816    #[test]
1817    fn test_socket_error_kind_bind_failed() {
1818        let kind = SocketErrorKind::BindFailed { port: 8080 };
1819        let display = format!("{}", kind);
1820        assert!(display.contains("failed to bind"));
1821        assert!(display.contains("8080"));
1822    }
1823
1824    #[test]
1825    fn test_socket_error_kind_bind_failed_after_retries() {
1826        let kind = SocketErrorKind::BindFailedAfterRetries {
1827            port: 9000,
1828            attempts: 5,
1829        };
1830        let display = format!("{}", kind);
1831        assert!(display.contains("failed to bind"));
1832        assert!(display.contains("9000"));
1833        assert!(display.contains('5'));
1834        assert!(display.contains("attempts"));
1835    }
1836
1837    #[test]
1838    fn test_socket_error_kind_custom() {
1839        let kind = SocketErrorKind::Custom("custom socket error");
1840        let display = format!("{}", kind);
1841        assert!(display.contains("custom socket error"));
1842    }
1843
1844    #[test]
1845    fn test_socket_error_kind_is_copy() {
1846        // Verify SocketErrorKind is Copy (important for hot path)
1847        let kind = SocketErrorKind::BindFailed { port: 8080 };
1848        let kind2 = kind; // Copy
1849        assert_eq!(kind, kind2);
1850    }
1851
1852    // =========================================================================
1853    // FortressError Structured Variant Tests
1854    // =========================================================================
1855
1856    #[test]
1857    fn test_invalid_request_structured_display() {
1858        let err = FortressError::InvalidRequestStructured {
1859            kind: InvalidRequestKind::ZeroPlayers,
1860        };
1861        let display = format!("{}", err);
1862        assert!(display.contains("Invalid Request"));
1863        assert!(display.contains("players"));
1864    }
1865
1866    #[test]
1867    fn test_serialization_error_structured_display() {
1868        let err = FortressError::SerializationErrorStructured {
1869            kind: SerializationErrorKind::EndpointCreationFailed,
1870        };
1871        let display = format!("{}", err);
1872        assert!(display.contains("Serialization error"));
1873        assert!(display.contains("endpoint"));
1874    }
1875
1876    #[test]
1877    fn test_socket_error_structured_display() {
1878        let err = FortressError::SocketErrorStructured {
1879            kind: SocketErrorKind::BindFailed { port: 8080 },
1880        };
1881        let display = format!("{}", err);
1882        assert!(display.contains("Socket error"));
1883        assert!(display.contains("8080"));
1884    }
1885
1886    // =========================================================================
1887    // From Implementations Tests
1888    // =========================================================================
1889
1890    #[test]
1891    fn test_from_invalid_request_kind() {
1892        let kind = InvalidRequestKind::ZeroPlayers;
1893        let err: FortressError = kind.into();
1894        assert_eq!(
1895            err,
1896            FortressError::InvalidRequestStructured {
1897                kind: InvalidRequestKind::ZeroPlayers
1898            }
1899        );
1900    }
1901
1902    #[test]
1903    fn test_from_serialization_error_kind() {
1904        let kind = SerializationErrorKind::EndpointCreationFailed;
1905        let err: FortressError = kind.into();
1906        assert_eq!(
1907            err,
1908            FortressError::SerializationErrorStructured {
1909                kind: SerializationErrorKind::EndpointCreationFailed
1910            }
1911        );
1912    }
1913
1914    #[test]
1915    fn test_from_socket_error_kind() {
1916        let kind = SocketErrorKind::BindFailed { port: 8080 };
1917        let err: FortressError = kind.into();
1918        assert_eq!(
1919            err,
1920            FortressError::SocketErrorStructured {
1921                kind: SocketErrorKind::BindFailed { port: 8080 }
1922            }
1923        );
1924    }
1925
1926    // =========================================================================
1927    // New Variant Tests (Review Feedback)
1928    // =========================================================================
1929
1930    #[test]
1931    fn test_invalid_request_kind_duration_config_out_of_range() {
1932        let kind = InvalidRequestKind::DurationConfigOutOfRange {
1933            field: "disconnect_timeout",
1934            min_ms: 100,
1935            max_ms: 60000,
1936            actual_ms: 50,
1937        };
1938        let display = format!("{}", kind);
1939        assert!(display.contains("duration configuration"));
1940        assert!(display.contains("disconnect_timeout"));
1941        assert!(display.contains("50ms"));
1942        assert!(display.contains("100ms"));
1943        assert!(display.contains("60000ms"));
1944    }
1945
1946    #[test]
1947    fn test_invalid_request_kind_invalid_local_player_handle() {
1948        let kind = InvalidRequestKind::InvalidLocalPlayerHandle {
1949            handle: PlayerHandle(5),
1950            num_players: 4,
1951        };
1952        let display = format!("{}", kind);
1953        assert!(display.contains("invalid local player handle"));
1954        assert!(display.contains('5'));
1955        assert!(display.contains("num_players is 4"));
1956    }
1957
1958    #[test]
1959    fn test_invalid_request_kind_invalid_remote_player_handle() {
1960        let kind = InvalidRequestKind::InvalidRemotePlayerHandle {
1961            handle: PlayerHandle(10),
1962            num_players: 2,
1963        };
1964        let display = format!("{}", kind);
1965        assert!(display.contains("invalid remote player handle"));
1966        assert!(display.contains("10"));
1967        assert!(display.contains("num_players is 2"));
1968    }
1969
1970    #[test]
1971    fn test_invalid_request_kind_invalid_spectator_handle() {
1972        let kind = InvalidRequestKind::InvalidSpectatorHandle {
1973            handle: PlayerHandle(3),
1974            num_players: 2,
1975        };
1976        let display = format!("{}", kind);
1977        assert!(display.contains("invalid spectator handle"));
1978        assert!(display.contains('3'));
1979        assert!(display.contains("num_players is 2"));
1980    }
1981
1982    #[test]
1983    fn test_invalid_request_kind_input_delay_too_large() {
1984        let kind = InvalidRequestKind::InputDelayTooLarge {
1985            delay_frames: 660,
1986            fps: 60,
1987            max_seconds_limit: 10,
1988        };
1989        let display = format!("{}", kind);
1990        assert!(display.contains("input delay"));
1991        assert!(display.contains("660"));
1992        assert!(display.contains("11.00s")); // 660 / 60 = 11.00
1993        assert!(display.contains("10s")); // max_seconds_limit
1994    }
1995
1996    #[test]
1997    fn test_invalid_request_kind_input_delay_too_large_with_zero_fps() {
1998        // Test the defensive branch that handles fps=0 without panicking
1999        let kind = InvalidRequestKind::InputDelayTooLarge {
2000            delay_frames: 100,
2001            fps: 0,
2002            max_seconds_limit: 10,
2003        };
2004        let display = format!("{}", kind);
2005        assert!(display.contains("input delay"));
2006        assert!(display.contains("100"));
2007        assert!(
2008            display.contains("inf"),
2009            "Should display infinity for zero fps: {display}"
2010        );
2011    }
2012
2013    #[test]
2014    fn test_new_variants_are_copy() {
2015        // Verify new variants remain Copy (important for hot path)
2016        let kind1 = InvalidRequestKind::DurationConfigOutOfRange {
2017            field: "test",
2018            min_ms: 0,
2019            max_ms: 100,
2020            actual_ms: 50,
2021        };
2022        let kind1_copy = kind1; // Copy
2023        assert_eq!(kind1, kind1_copy);
2024
2025        let kind2 = InvalidRequestKind::InvalidLocalPlayerHandle {
2026            handle: PlayerHandle(1),
2027            num_players: 2,
2028        };
2029        let kind2_copy = kind2; // Copy
2030        assert_eq!(kind2, kind2_copy);
2031
2032        let kind3 = InvalidRequestKind::InputDelayTooLarge {
2033            delay_frames: 10,
2034            fps: 60,
2035            max_seconds_limit: 1,
2036        };
2037        let kind3_copy = kind3; // Copy
2038        assert_eq!(kind3, kind3_copy);
2039    }
2040
2041    // ==========================================
2042    // Frame Arithmetic Error Tests
2043    // ==========================================
2044
2045    #[test]
2046    fn test_frame_arithmetic_overflow_display() {
2047        let err = FortressError::FrameArithmeticOverflow {
2048            frame: Frame::new(i32::MAX),
2049            operand: 1,
2050            operation: "add",
2051        };
2052        let display = format!("{}", err);
2053        assert!(display.contains("Frame arithmetic overflow"));
2054        assert!(display.contains("add"));
2055        assert!(display.contains('1'));
2056    }
2057
2058    #[test]
2059    fn test_frame_arithmetic_overflow_sub_display() {
2060        let err = FortressError::FrameArithmeticOverflow {
2061            frame: Frame::new(i32::MIN),
2062            operand: 1,
2063            operation: "sub",
2064        };
2065        let display = format!("{}", err);
2066        assert!(display.contains("Frame arithmetic overflow"));
2067        assert!(display.contains("sub"));
2068    }
2069
2070    #[test]
2071    fn test_frame_value_too_large_display() {
2072        let too_large = (i32::MAX as usize) + 1;
2073        let err = FortressError::FrameValueTooLarge { value: too_large };
2074        let display = format!("{}", err);
2075        assert!(display.contains("Frame value too large"));
2076        assert!(display.contains(&too_large.to_string()));
2077        assert!(display.contains(&i32::MAX.to_string()));
2078    }
2079
2080    #[test]
2081    fn test_frame_arithmetic_overflow_debug() {
2082        let err = FortressError::FrameArithmeticOverflow {
2083            frame: Frame::new(100),
2084            operand: 50,
2085            operation: "add",
2086        };
2087        let debug = format!("{:?}", err);
2088        assert!(debug.contains("FrameArithmeticOverflow"));
2089        assert!(debug.contains("100"));
2090        assert!(debug.contains("50"));
2091        assert!(debug.contains("add"));
2092    }
2093
2094    #[test]
2095    fn test_frame_value_too_large_debug() {
2096        let err = FortressError::FrameValueTooLarge { value: 12345 };
2097        let debug = format!("{:?}", err);
2098        assert!(debug.contains("FrameValueTooLarge"));
2099        assert!(debug.contains("12345"));
2100    }
2101
2102    #[test]
2103    fn test_frame_errors_are_clone_and_eq() {
2104        let err1 = FortressError::FrameArithmeticOverflow {
2105            frame: Frame::new(100),
2106            operand: 1,
2107            operation: "add",
2108        };
2109        let err1_clone = err1.clone();
2110        assert_eq!(err1, err1_clone);
2111
2112        let err2 = FortressError::FrameValueTooLarge { value: 999 };
2113        let err2_clone = err2.clone();
2114        assert_eq!(err2, err2_clone);
2115    }
2116}