ghost_sync/
types.rs

1use bytes::Bytes;
2use std::time::Duration;
3use uuid::Uuid;
4use wincode::{SchemaRead, SchemaWrite};
5
6/// Messages clients send to the server.
7#[derive(SchemaWrite, SchemaRead)]
8pub enum ClientWire {
9    JoinRoom { room_id: String, data: Vec<u8> },
10    LeaveRoom,
11    Ping,
12    Pong,
13    Broadcast { data: Vec<u8> },
14    EchoTest { data: Vec<u8> },
15}
16
17/// Messages the server sends to clients.
18#[derive(SchemaWrite, SchemaRead)]
19pub enum ServerWire {
20    Joined { client_id: Uuid, room_id: String },
21    PlayerJoined { client_id: Uuid },
22    PlayerLeft { client_id: Uuid },
23    Ping,
24    Pong,
25    Error(String),
26    Broadcast { sender_id: Uuid, data: Vec<u8> },
27    EchoTest { data: Vec<u8> },
28}
29
30#[allow(rustdoc::private_intra_doc_links)]
31/// Client-facing event (clean API over [`ServerWire`])
32/// Events the client receives from the server.
33#[derive(Debug)]
34pub enum ServerEvent {
35    /// Room join confirmed.
36    Joined { client_id: Uuid, room_id: String },
37    /// Another player joined your room.
38    PlayerJoined { client_id: Uuid },
39    /// Another player left your room.
40    PlayerLeft { client_id: Uuid },
41    /// Server error.
42    Error(String),
43    /// Relayed data from a peer.
44    Broadcast { sender_id: Uuid, data: Bytes },
45    /// Health Echo Check from server
46    EchoTest { data: Bytes },
47}
48
49/// Server configuration.
50#[derive(Clone)]
51pub struct ServerConfig {
52    pub bind_addr: String,
53    pub max_clients: usize,
54    pub max_payload: usize,
55    /// Max time without receiving any frame from a client.
56    /// This is a raw read inactivity timeout: if the server gets nothing for
57    /// this duration, the client is disconnected with [`SyncError::IdleTimeout`].
58    ///
59    /// Pong responses to server pings count as activity and reset this timer.
60    ///
61    /// See also: [`ping_interval`](Self::ping_interval) for protocol-level
62    /// liveness (server sends Ping, expects Pong before next tick).
63    pub idle_timeout: Duration,
64    /// How often the server sends Ping to clients. The pong deadline is one
65    /// full `ping_interval` — if no Pong arrives before the next tick, the
66    /// client is disconnected with [`SyncError::PingTimeout`].
67    pub ping_interval: Duration,
68    /// Per-client write channel capacity. Frames are dropped when the channel
69    /// is full (with `on_backpressure` hook). Higher values buffer more for
70    /// bursty games; lower values keep latency tight.
71    pub channel_capacity: usize,
72}
73
74impl Default for ServerConfig {
75    fn default() -> Self {
76        Self {
77            bind_addr: "0.0.0.0:7777".into(),
78            max_clients: 1024,
79            max_payload: 64 * 1024,
80            // Non-divisible defaults to avoid idle_timeout and ping_interval
81            idle_timeout: Duration::from_secs(31),
82            ping_interval: Duration::from_secs(13),
83            channel_capacity: 1024,
84        }
85    }
86}
87
88/// Errors that can occur during client-server communication and protocol handling.
89pub enum SyncError {
90    PayloadTooLarge { size: usize, max: usize },
91    IdleTimeout,
92    PingTimeout,
93    RoomNotFound,
94    RoomAlreadyExists(String),
95    NotInRoom,
96    MaxClientsReached,
97    ConnectionClosed,
98    ConnectionRefused,
99    Protocol(String),
100    Io(std::io::Error),
101}
102
103impl SyncError {
104    /// Returns true if this is a "normal" connection close (not an actual error).
105    /// This includes EOF, connection reset, broken pipe, and connection aborted.
106    pub fn is_connection_closed(&self) -> bool {
107        match self {
108            Self::ConnectionClosed => true,
109            Self::Io(e) => is_graceful_io_error(e),
110            _ => false,
111        }
112    }
113}
114
115/// Returns true if an IO error represents a normal connection close.
116///
117/// Detects:
118/// - `UnexpectedEof` — remote closed cleanly
119/// - `ConnectionReset` — remote forcibly closed (10054 Windows / ECONNRESET Unix)
120/// - `ConnectionAborted` — connection aborted (10053 Windows / ECONNABORTED Unix)
121/// - `BrokenPipe` — broken pipe (EPIPE Unix)
122/// - Any IO error with OS error codes matching the above
123pub(crate) fn is_graceful_io_error(e: &std::io::Error) -> bool {
124    match e.kind() {
125        std::io::ErrorKind::UnexpectedEof
126        | std::io::ErrorKind::ConnectionReset
127        | std::io::ErrorKind::ConnectionAborted
128        | std::io::ErrorKind::BrokenPipe => true,
129        _ => {
130            // Also check raw OS error codes for cross-platform coverage
131            matches!(
132                e.raw_os_error(),
133                Some(
134                    10054 | // Windows WSAECONNRESET
135                    10053 | // Windows WSAECONNABORTED
136                    10058 // Windows WSAESHUTDOWN
137                )
138            )
139        }
140    }
141}
142
143impl std::fmt::Debug for SyncError {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        std::fmt::Display::fmt(self, f)
146    }
147}
148
149impl std::fmt::Display for SyncError {
150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151        match self {
152            Self::PayloadTooLarge { size, max } => {
153                write!(f, "payload too large: {} bytes (max {})", size, max)
154            }
155            Self::IdleTimeout => write!(f, "client idle timeout"),
156            Self::PingTimeout => write!(f, "no pong received before next ping tick"),
157            Self::RoomNotFound => write!(f, "room not found"),
158            Self::RoomAlreadyExists(id) => write!(f, "room already exists: {id}"),
159            Self::NotInRoom => write!(f, "client not in a room"),
160            Self::MaxClientsReached => write!(f, "max clients reached"),
161            Self::ConnectionClosed => write!(f, "connection closed"),
162            Self::ConnectionRefused => write!(f, "connection refused — is the server running?"),
163            Self::Protocol(msg) => write!(f, "protocol error: {}", msg),
164            Self::Io(e) => write!(f, "io error: {}", e),
165        }
166    }
167}
168
169impl std::error::Error for SyncError {}
170
171impl From<std::io::Error> for SyncError {
172    fn from(e: std::io::Error) -> Self {
173        Self::Io(e)
174    }
175}
176
177pub type Result<T> = std::result::Result<T, SyncError>;