signal_fish_client/protocol.rs
1//! Wire-compatible protocol types for the Signal Fish signaling protocol.
2//!
3//! Every type in this module produces identical JSON to the server's
4//! `protocol::messages` and `protocol::types` modules. Key adaptations:
5//!
6//! - `bytes::Bytes` → `Vec<u8>` with `#[serde(with = "serde_bytes")]`
7//! - `chrono::DateTime<Utc>` → `String` (ISO 8601)
8//! - No `rkyv` derives (server-only concern)
9
10use serde::{Deserialize, Deserializer, Serialize};
11use uuid::Uuid;
12
13use crate::error_codes::ErrorCode;
14
15pub mod binary;
16pub use binary::{
17 decode_v2_binary_game_data, decode_v3_binary_game_data, V2BinaryGameDataFrame,
18 V3BinaryGameDataFrame,
19};
20
21// ── Type aliases ────────────────────────────────────────────────────
22
23/// Unique identifier for players.
24pub type PlayerId = Uuid;
25
26/// Unique identifier for rooms.
27pub type RoomId = Uuid;
28
29// ── Enums ───────────────────────────────────────────────────────────
30
31/// Relay transport protocol selection.
32#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
33#[serde(rename_all = "lowercase")]
34pub enum RelayTransport {
35 /// TCP transport (reliable, ordered delivery).
36 /// Recommended for: Turn-based games, lobby systems, RPGs.
37 Tcp,
38 /// UDP transport (low-latency, unreliable).
39 /// Recommended for: FPS, racing games, real-time action.
40 Udp,
41 /// WebSocket transport (reliable, browser-compatible).
42 /// Recommended for: WebGL builds, browser games, cross-platform.
43 Websocket,
44 /// Automatic selection based on room size and game type.
45 /// Default: UDP for 2-4 players, TCP for 5+ players, WebSocket for browser builds.
46 #[default]
47 Auto,
48}
49
50/// Encoding format for sequenced game data payloads.
51#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
52#[serde(rename_all = "snake_case")]
53pub enum GameDataEncoding {
54 /// JSON payloads delivered over text frames.
55 #[default]
56 Json,
57 /// MessagePack payloads delivered over binary frames.
58 #[serde(rename = "message_pack")]
59 MessagePack,
60 /// Rkyv zero-copy binary format.
61 ///
62 /// **Caveat:** the current server never advertises or negotiates rkyv —
63 /// requesting it does not fail, it silently downgrades to JSON (the
64 /// server's `game_data_formats` will not include `"rkyv"`). Treat this
65 /// variant as reserved until the server ships rkyv negotiation.
66 #[serde(rename = "rkyv")]
67 Rkyv,
68}
69
70/// Protocol-v3 delivery policy for relayed game data.
71#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum DeliveryClass {
74 /// Preserve every message or disconnect the recipient loudly.
75 #[default]
76 Reliable,
77 /// Retain only the newest undelivered value for a sender-defined key.
78 Latest,
79 /// Deliver opportunistically without sender backpressure.
80 Volatile,
81}
82
83/// Why an exact server-stamped sequence range was omitted.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
85#[serde(rename_all = "snake_case")]
86pub enum DeliveryGapReason {
87 LatestSuperseded,
88 LatestDroppedFull,
89 VolatileDropped,
90 UnsupportedFormat,
91}
92
93/// Exact inclusive sequence range omitted for one recipient.
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95pub struct DeliveryGap {
96 pub from_player: PlayerId,
97 pub epoch: u32,
98 pub from_seq: u64,
99 pub to_seq: u64,
100 pub reason: DeliveryGapReason,
101}
102
103/// Cumulative reliable-delivery outcomes.
104#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
105pub struct ReliableDeliveryCounters {
106 pub delivered: u64,
107 pub abandoned: u64,
108 pub unsupported_format: u64,
109}
110
111/// Cumulative keyed-latest delivery outcomes.
112#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
113pub struct LatestDeliveryCounters {
114 pub delivered: u64,
115 pub superseded: u64,
116 pub dropped_full: u64,
117 pub abandoned: u64,
118 pub unsupported_format: u64,
119}
120
121/// Cumulative volatile-delivery outcomes.
122#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
123pub struct VolatileDeliveryCounters {
124 pub delivered: u64,
125 pub dropped: u64,
126 pub abandoned: u64,
127 pub unsupported_format: u64,
128}
129
130/// Cumulative delivery outcomes partitioned by delivery class.
131#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
132pub struct DeliveryCountersByClass {
133 pub reliable: ReliableDeliveryCounters,
134 pub latest: LatestDeliveryCounters,
135 pub volatile: VolatileDeliveryCounters,
136}
137
138/// Exact protocol-v3 accountability report.
139#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
140pub struct DeliveryReportPayload {
141 pub per_class: DeliveryCountersByClass,
142 #[serde(default, skip_serializing_if = "Vec::is_empty")]
143 pub gaps: Vec<DeliveryGap>,
144}
145
146/// Maximum exact omission ranges carried in one delivery report.
147pub const DELIVERY_REPORT_MAX_GAPS: usize = 256;
148
149/// Completeness of the control-event replay on reconnect.
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(rename_all = "snake_case")]
152pub enum ReplayStatus {
153 Complete,
154 Truncated,
155 Unavailable,
156}
157
158/// Authoritative per-sender relay baseline supplied on reconnect.
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
160pub struct SenderWatermark {
161 pub player_id: PlayerId,
162 pub epoch: u32,
163 pub seq: u64,
164}
165
166/// Session topology selected by the server for a finalized room (protocol v3).
167///
168/// The server is authoritative: it chooses the topology and communicates the
169/// result via [`SessionPlanPayload`]. The client obeys it; it never computes a
170/// topology itself.
171#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
172#[serde(rename_all = "snake_case")]
173pub enum Topology {
174 /// Server relay hub — the v2 behavior, always available (the "relay floor").
175 Relay,
176 /// Star topology around a single elected host/authority.
177 Host,
178 /// Full mesh: every peer connects to every other peer.
179 Mesh,
180}
181
182/// Data-path transport selected by the server for a finalized room (protocol v3).
183///
184/// Distinct from the [`Transport`](crate::Transport) I/O trait: `TransportKind`
185/// is a wire *value* the server sends to describe how peers should exchange game
186/// data, whereas `Transport` is the byte channel to the signaling server.
187#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
188#[serde(rename_all = "snake_case")]
189pub enum TransportKind {
190 /// Server WebSocket fan-out — the mandatory floor every client supports.
191 Relay,
192 /// Direct IP:port connection (LAN / routable host).
193 Direct,
194 /// Peer-to-peer WebRTC data channel.
195 ///
196 /// `rename_all = "snake_case"` would emit `web_rtc`; the protocol requires
197 /// the token `webrtc`, so the variant is renamed explicitly. This token
198 /// deliberately matches [`ConnectionInfo::WebRTC`]'s `#[serde(rename = "webrtc")]`.
199 #[serde(rename = "webrtc")]
200 WebRtc,
201}
202
203/// Signaling-message transport exposed by the server for this connection.
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
205#[serde(rename_all = "lowercase")]
206pub enum MessageTransport {
207 /// RFC 6455 WebSocket text/binary frames.
208 Websocket,
209}
210
211/// Connection information for P2P establishment.
212#[derive(Debug, Clone, Serialize, Deserialize)]
213#[serde(tag = "type")]
214pub enum ConnectionInfo {
215 /// Direct IP:port connection (for Mirror, FishNet, Unity NetCode direct).
216 #[serde(rename = "direct")]
217 Direct { host: String, port: u16 },
218 /// Unity Relay allocation (for Unity NetCode via Unity Relay).
219 #[serde(rename = "unity_relay")]
220 UnityRelay {
221 allocation_id: String,
222 connection_data: String,
223 key: String,
224 },
225 /// Built-in relay server (for Unity NetCode, FishNet, Mirror).
226 #[serde(rename = "relay")]
227 Relay {
228 /// Relay server host.
229 host: String,
230 /// Relay server port (TCP or UDP depending on transport).
231 port: u16,
232 /// Transport protocol (TCP, UDP, or Auto).
233 #[serde(default)]
234 transport: RelayTransport,
235 /// Allocation ID (room ID).
236 allocation_id: String,
237 /// Client authentication token (opaque server-issued value).
238 token: String,
239 /// Assigned client ID (set by server after connection).
240 #[serde(skip_serializing_if = "Option::is_none")]
241 client_id: Option<u16>,
242 },
243 /// WebRTC connection info (for Matchbox).
244 #[serde(rename = "webrtc")]
245 WebRTC {
246 sdp: Option<String>,
247 ice_candidates: Vec<String>,
248 },
249 /// Custom connection data (extensible for other types).
250 #[serde(rename = "custom")]
251 Custom { data: serde_json::Value },
252}
253
254/// Describes why a spectator state change occurred.
255#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
256#[serde(rename_all = "snake_case")]
257pub enum SpectatorStateChangeReason {
258 #[default]
259 Joined,
260 VoluntaryLeave,
261 Disconnected,
262 Removed,
263 RoomClosed,
264}
265
266/// Lobby readiness state.
267#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
268#[serde(rename_all = "snake_case")]
269pub enum LobbyState {
270 #[default]
271 Waiting,
272 Lobby,
273 Finalized,
274}
275
276// ── Structs ─────────────────────────────────────────────────────────
277
278/// Information about a player in a room.
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct PlayerInfo {
281 pub id: PlayerId,
282 pub name: String,
283 pub is_authority: bool,
284 pub is_ready: bool,
285 pub connected_at: String,
286 /// Connection info for P2P establishment (provided when player is ready).
287 #[serde(skip_serializing_if = "Option::is_none")]
288 pub connection_info: Option<ConnectionInfo>,
289 /// Current server-tracked incarnation epoch (protocol v3 only).
290 #[serde(default, skip_serializing_if = "Option::is_none")]
291 pub epoch: Option<u32>,
292 /// Relay tail represented by this snapshot (protocol v3 only).
293 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub seq: Option<u64>,
295}
296
297/// Information about a spectator watching a room.
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct SpectatorInfo {
300 pub id: PlayerId,
301 pub name: String,
302 pub connected_at: String,
303}
304
305/// Peer connection information for game start.
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct PeerConnectionInfo {
308 pub player_id: PlayerId,
309 pub player_name: String,
310 pub is_authority: bool,
311 pub relay_type: String,
312 /// Connection info provided by the peer for P2P establishment.
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub connection_info: Option<ConnectionInfo>,
315}
316
317/// Rate limit information for an application.
318#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct RateLimitInfo {
320 /// Requests allowed per minute.
321 pub per_minute: u32,
322 /// Requests allowed per hour.
323 pub per_hour: u32,
324 /// Requests allowed per day.
325 pub per_day: u32,
326}
327
328/// Describes negotiated protocol capabilities for a specific SDK.
329#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct ProtocolInfoPayload {
331 #[serde(skip_serializing_if = "Option::is_none")]
332 pub platform: Option<String>,
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub sdk_version: Option<String>,
335 #[serde(skip_serializing_if = "Option::is_none")]
336 pub minimum_version: Option<String>,
337 #[serde(skip_serializing_if = "Option::is_none")]
338 pub recommended_version: Option<String>,
339 #[serde(default)]
340 pub capabilities: Vec<String>,
341 #[serde(skip_serializing_if = "Option::is_none")]
342 pub notes: Option<String>,
343 #[serde(default)]
344 pub game_data_formats: Vec<GameDataEncoding>,
345 #[serde(skip_serializing_if = "Option::is_none")]
346 pub player_name_rules: Option<PlayerNameRulesPayload>,
347 /// Protocol version negotiated for this connection (protocol v3+ only).
348 ///
349 /// `None` for a negotiated v2 connection, keeping the v2 wire contract
350 /// byte-identical.
351 #[serde(
352 default,
353 skip_serializing_if = "Option::is_none",
354 deserialize_with = "deserialize_present_optional"
355 )]
356 pub protocol_version: Option<u16>,
357 /// Lowest protocol version this deployment accepts (protocol v3+ only).
358 #[serde(
359 default,
360 skip_serializing_if = "Option::is_none",
361 deserialize_with = "deserialize_present_optional"
362 )]
363 pub min_protocol_version: Option<u16>,
364 /// Highest protocol version this deployment speaks (protocol v3+ only).
365 #[serde(
366 default,
367 skip_serializing_if = "Option::is_none",
368 deserialize_with = "deserialize_present_optional"
369 )]
370 pub max_protocol_version: Option<u16>,
371 /// Server message transports available to this connection (v3 only).
372 #[serde(
373 default,
374 skip_serializing_if = "Option::is_none",
375 deserialize_with = "deserialize_present_optional"
376 )]
377 pub transports: Option<Vec<MessageTransport>>,
378}
379
380/// Describes the characters a deployment allows inside `player_name`.
381#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct PlayerNameRulesPayload {
383 pub max_length: usize,
384 pub min_length: usize,
385 pub allow_unicode_alphanumeric: bool,
386 pub allow_spaces: bool,
387 pub allow_leading_trailing_whitespace: bool,
388 #[serde(default)]
389 pub allowed_symbols: Vec<char>,
390 #[serde(skip_serializing_if = "Option::is_none")]
391 pub additional_allowed_characters: Option<String>,
392}
393
394/// A STUN/TURN server for WebRTC ICE negotiation (protocol v3).
395///
396/// `username`/`credential` are present only for TURN servers; bare STUN entries
397/// omit them, keeping the wire bytes minimal.
398#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
399pub struct IceServer {
400 /// STUN/TURN URLs (e.g. `stun:stun.l.google.com:19302`).
401 pub urls: Vec<String>,
402 /// TURN username (omitted for credential-less STUN servers).
403 #[serde(skip_serializing_if = "Option::is_none")]
404 pub username: Option<String>,
405 /// TURN credential (omitted for credential-less STUN servers).
406 #[serde(skip_serializing_if = "Option::is_none")]
407 pub credential: Option<String>,
408}
409
410/// A peer the recipient should connect to within a [`SessionPlanPayload`] (protocol v3).
411#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
412pub struct SessionPeer {
413 /// The other peer's identifier.
414 pub player_id: PlayerId,
415 /// The other peer's display name.
416 pub player_name: String,
417 /// Whether this peer is the session's authoritative host.
418 pub is_authority: bool,
419 /// Whether the recipient sends the WebRTC offer to this peer.
420 ///
421 /// Server-assigned (a deterministic "designated offerer"). Obey it
422 /// verbatim — the client never computes who initiates.
423 pub initiate: bool,
424}
425
426// ── Payload structs ─────────────────────────────────────────────────
427
428/// Payload for the `RoomJoined` server message.
429/// Boxed in `ServerMessage` to reduce enum size.
430#[derive(Debug, Clone, Serialize, Deserialize)]
431pub struct RoomJoinedPayload {
432 pub room_id: RoomId,
433 pub room_code: String,
434 pub player_id: PlayerId,
435 pub game_name: String,
436 pub max_players: u8,
437 pub supports_authority: bool,
438 pub current_players: Vec<PlayerInfo>,
439 pub is_authority: bool,
440 pub lobby_state: LobbyState,
441 pub ready_players: Vec<PlayerId>,
442 pub relay_type: String,
443 /// List of spectators currently watching (if any).
444 #[serde(default)]
445 pub current_spectators: Vec<SpectatorInfo>,
446 /// ICE (STUN/TURN) servers for early WebRTC candidate gathering during the
447 /// lobby wait (protocol v3 only; "ICE pre-gather"). Empty — and absent from
448 /// the wire via `skip_serializing_if` — for v2 connections, keeping the v2
449 /// JSON byte-identical.
450 #[serde(default, skip_serializing_if = "Vec::is_empty")]
451 pub ice_servers: Vec<IceServer>,
452 /// Server-issued token for a later unexpected-disconnect reconnect.
453 #[serde(default, skip_serializing_if = "Option::is_none")]
454 pub reconnection_token: Option<String>,
455}
456
457/// Payload for the `Reconnected` server message.
458/// Boxed in `ServerMessage` to reduce enum size.
459#[derive(Debug, Clone, Serialize, Deserialize)]
460pub struct ReconnectedPayload {
461 pub room_id: RoomId,
462 pub room_code: String,
463 pub player_id: PlayerId,
464 pub game_name: String,
465 pub max_players: u8,
466 pub supports_authority: bool,
467 pub current_players: Vec<PlayerInfo>,
468 pub is_authority: bool,
469 pub lobby_state: LobbyState,
470 pub ready_players: Vec<PlayerId>,
471 pub relay_type: String,
472 /// List of spectators currently watching (if any).
473 #[serde(default)]
474 pub current_spectators: Vec<SpectatorInfo>,
475 /// ICE (STUN/TURN) servers for early WebRTC candidate gathering (protocol
476 /// v3 only). Empty — and absent from the wire — for v2 connections.
477 #[serde(default, skip_serializing_if = "Vec::is_empty")]
478 pub ice_servers: Vec<IceServer>,
479 /// Events that occurred while disconnected.
480 pub missed_events: Vec<ServerMessage>,
481 /// Completeness of `missed_events` (protocol v3 only).
482 #[serde(default, skip_serializing_if = "Option::is_none")]
483 pub replay: Option<ReplayStatus>,
484 /// Authoritative relay baselines for every current player (v3 only).
485 #[serde(default, skip_serializing_if = "Vec::is_empty")]
486 pub sender_watermarks: Vec<SenderWatermark>,
487 /// Fresh token replacing the consumed reconnect token (v3 only).
488 #[serde(default, skip_serializing_if = "Option::is_none")]
489 pub reconnection_token: Option<String>,
490}
491
492/// Payload for the `SpectatorJoined` server message.
493/// Boxed in `ServerMessage` to reduce enum size.
494#[derive(Debug, Clone, Serialize, Deserialize)]
495pub struct SpectatorJoinedPayload {
496 pub room_id: RoomId,
497 pub room_code: String,
498 pub spectator_id: PlayerId,
499 pub game_name: String,
500 pub current_players: Vec<PlayerInfo>,
501 pub current_spectators: Vec<SpectatorInfo>,
502 pub lobby_state: LobbyState,
503 #[serde(skip_serializing_if = "Option::is_none")]
504 pub reason: Option<SpectatorStateChangeReason>,
505}
506
507/// Payload for the `SessionPlan` server message (protocol v3).
508/// Boxed in `ServerMessage` to reduce enum size.
509///
510/// Sent per-recipient when a room finalizes to a non-relay session (and again
511/// on late-join or host re-election). Each recipient receives a plan tailored
512/// to it: `peers` excludes the recipient, and each `initiate` flag is set from
513/// the recipient's perspective.
514#[derive(Debug, Clone, Serialize, Deserialize)]
515pub struct SessionPlanPayload {
516 /// Chosen session topology (`relay`, `host`, or `mesh`).
517 pub topology: Topology,
518 /// Chosen data-path transport (`relay`, `direct`, or `webrtc`).
519 pub transport: TransportKind,
520 /// The elected host, present only for `host` topology.
521 #[serde(skip_serializing_if = "Option::is_none")]
522 pub host: Option<PlayerId>,
523 /// Peers this recipient should connect to (excludes the recipient itself).
524 pub peers: Vec<SessionPeer>,
525 /// ICE (STUN/TURN) servers for WebRTC; omitted for non-WebRTC plans.
526 #[serde(default, skip_serializing_if = "Vec::is_empty")]
527 pub ice_servers: Vec<IceServer>,
528 /// The universal fallback transport — always [`TransportKind::Relay`], the floor.
529 pub fallback: TransportKind,
530}
531
532// ── Messages ────────────────────────────────────────────────────────
533
534/// Message types sent from client to server.
535#[derive(Debug, Clone, Serialize, Deserialize)]
536#[serde(tag = "type", content = "data")]
537pub enum ClientMessage {
538 /// Authenticate with App ID (MUST be first message).
539 /// App ID is a public identifier (not a secret!) that identifies the game application.
540 Authenticate {
541 /// Public App ID (safe to embed in game builds, e.g., "mb_app_abc123...").
542 app_id: String,
543 /// SDK version for debugging and analytics.
544 #[serde(skip_serializing_if = "Option::is_none")]
545 sdk_version: Option<String>,
546 /// Platform information (e.g., "unity", "godot", "unreal").
547 #[serde(skip_serializing_if = "Option::is_none")]
548 platform: Option<String>,
549 /// Preferred game data encoding (defaults to JSON text frames).
550 #[serde(skip_serializing_if = "Option::is_none")]
551 game_data_format: Option<GameDataEncoding>,
552 /// Highest protocol version the client speaks (protocol v3+).
553 ///
554 /// Omitted by default so the server treats the client as v2 relay-only
555 /// and the wire bytes stay identical to v2.
556 #[serde(skip_serializing_if = "Option::is_none")]
557 protocol_version: Option<u16>,
558 /// Data-path transports the client can actually fulfill (protocol v3+).
559 #[serde(skip_serializing_if = "Option::is_none")]
560 supported_transports: Option<Vec<TransportKind>>,
561 /// Session topologies the client can participate in (protocol v3+).
562 #[serde(skip_serializing_if = "Option::is_none")]
563 supported_topologies: Option<Vec<Topology>>,
564 },
565 /// Join or create a room for a specific game.
566 JoinRoom {
567 game_name: String,
568 room_code: Option<String>,
569 player_name: String,
570 max_players: Option<u8>,
571 supports_authority: Option<bool>,
572 /// Preferred relay transport protocol (TCP, UDP, or Auto).
573 /// If not specified, defaults to Auto.
574 #[serde(default)]
575 relay_transport: Option<RelayTransport>,
576 },
577 /// Leave the current room.
578 LeaveRoom,
579 /// Send game data to other players in the room.
580 GameData {
581 data: serde_json::Value,
582 #[serde(
583 default,
584 deserialize_with = "deserialize_present_optional",
585 skip_serializing_if = "Option::is_none"
586 )]
587 class: Option<DeliveryClass>,
588 #[serde(
589 default,
590 deserialize_with = "deserialize_present_optional",
591 skip_serializing_if = "Option::is_none"
592 )]
593 key: Option<u32>,
594 },
595 /// Request to become or connect to authoritative server.
596 AuthorityRequest { become_authority: bool },
597 /// Signal readiness to start the game in lobby.
598 PlayerReady,
599 /// Provide connection info for P2P establishment.
600 ProvideConnectionInfo { connection_info: ConnectionInfo },
601 /// Heartbeat to maintain connection.
602 Ping,
603 /// Reconnect to a room after disconnection.
604 Reconnect {
605 player_id: PlayerId,
606 room_id: RoomId,
607 /// Authentication token generated on initial join.
608 auth_token: String,
609 },
610 /// Join a room as a spectator (read-only observer).
611 JoinAsSpectator {
612 game_name: String,
613 room_code: String,
614 spectator_name: String,
615 },
616 /// Leave spectator mode.
617 LeaveSpectator,
618 /// Explicitly start the game, finalizing the lobby with its current members
619 /// (protocol v2).
620 ///
621 /// Accepted only when every current player is ready. If the room has a
622 /// designated authority, only that authority may start; otherwise any
623 /// member may. On success the server broadcasts `GameStarting` (and, for a
624 /// negotiated v3 non-relay room, a per-recipient `SessionPlan`).
625 StartGame,
626 /// Relay an opaque WebRTC signal to a single peer.
627 ///
628 /// **Protocol v3 only.** Rejected on a relay-floor (v2) connection.
629 ///
630 /// The `signal` is forwarded verbatim by the server. It is typically a
631 /// [`PeerSignal`](crate::PeerSignal) (`{"Offer"|"Answer"|"IceCandidate": …}`)
632 /// but is modeled as `serde_json::Value` so unknown future shapes never
633 /// fail to round-trip.
634 Signal {
635 /// The recipient peer.
636 to: PlayerId,
637 /// The opaque signal payload (offer/answer/ICE candidate).
638 signal: serde_json::Value,
639 },
640 /// Report whether a data-path transport to peers is currently established.
641 ///
642 /// **Protocol v3 only.** Informational; the server fans it out as
643 /// `PeerTransportStatus` and uses it for fallback decisions.
644 TransportStatus {
645 /// The transport being reported on.
646 transport: TransportKind,
647 /// Whether that transport is currently connected.
648 connected: bool,
649 },
650}
651
652/// Message types sent from server to client.
653#[derive(Debug, Clone, Serialize, Deserialize)]
654#[serde(tag = "type", content = "data")]
655pub enum ServerMessage {
656 /// Authentication successful.
657 Authenticated {
658 /// App name for confirmation.
659 app_name: String,
660 /// Organization name (if any).
661 #[serde(skip_serializing_if = "Option::is_none")]
662 organization: Option<String>,
663 /// Rate limits for this app.
664 rate_limits: RateLimitInfo,
665 },
666 /// SDK/protocol compatibility details advertised after authentication.
667 ProtocolInfo(ProtocolInfoPayload),
668 /// Authentication failed.
669 AuthenticationError {
670 /// Error message.
671 error: String,
672 /// Error code for programmatic handling.
673 error_code: ErrorCode,
674 },
675 /// Successfully joined a room (boxed to reduce enum size).
676 RoomJoined(Box<RoomJoinedPayload>),
677 /// Failed to join room.
678 RoomJoinFailed {
679 reason: String,
680 #[serde(skip_serializing_if = "Option::is_none")]
681 error_code: Option<ErrorCode>,
682 },
683 /// Successfully left room.
684 RoomLeft,
685 /// Another player joined the room.
686 PlayerJoined { player: PlayerInfo },
687 /// Another player left the room.
688 PlayerLeft {
689 player_id: PlayerId,
690 #[serde(default, skip_serializing_if = "Option::is_none")]
691 epoch: Option<u32>,
692 #[serde(default, skip_serializing_if = "Option::is_none")]
693 final_seq: Option<u64>,
694 },
695 /// Game data from another player.
696 GameData {
697 from_player: PlayerId,
698 data: serde_json::Value,
699 #[serde(default, skip_serializing_if = "Option::is_none")]
700 seq: Option<u64>,
701 #[serde(default, skip_serializing_if = "Option::is_none")]
702 epoch: Option<u32>,
703 #[serde(default, skip_serializing_if = "Option::is_none")]
704 class: Option<DeliveryClass>,
705 #[serde(default, skip_serializing_if = "Option::is_none")]
706 key: Option<u32>,
707 },
708 /// Binary game data payload from another player.
709 /// Uses `Vec<u8>` with `serde_bytes` for efficient serialization.
710 GameDataBinary {
711 from_player: PlayerId,
712 encoding: GameDataEncoding,
713 #[serde(with = "serde_bytes")]
714 payload: Vec<u8>,
715 #[serde(default, skip_serializing_if = "Option::is_none")]
716 seq: Option<u64>,
717 #[serde(default, skip_serializing_if = "Option::is_none")]
718 epoch: Option<u32>,
719 },
720 /// Authority status changed.
721 AuthorityChanged {
722 authority_player: Option<PlayerId>,
723 you_are_authority: bool,
724 },
725 /// Authority request response.
726 AuthorityResponse {
727 granted: bool,
728 reason: Option<String>,
729 #[serde(skip_serializing_if = "Option::is_none")]
730 error_code: Option<ErrorCode>,
731 },
732 /// Lobby state changed (room full, player readiness changed, etc.).
733 LobbyStateChanged {
734 lobby_state: LobbyState,
735 ready_players: Vec<PlayerId>,
736 all_ready: bool,
737 },
738 /// Game is starting with peer connection information.
739 GameStarting {
740 peer_connections: Vec<PeerConnectionInfo>,
741 },
742 /// Pong response to ping.
743 Pong,
744 /// Reconnection successful (boxed to reduce enum size).
745 Reconnected(Box<ReconnectedPayload>),
746 /// Reconnection failed.
747 ReconnectionFailed {
748 reason: String,
749 error_code: ErrorCode,
750 },
751 /// Another player reconnected to the room.
752 PlayerReconnected {
753 player_id: PlayerId,
754 #[serde(default, skip_serializing_if = "Option::is_none")]
755 epoch: Option<u32>,
756 },
757 /// Successfully joined a room as spectator (boxed to reduce enum size).
758 SpectatorJoined(Box<SpectatorJoinedPayload>),
759 /// Failed to join as spectator.
760 SpectatorJoinFailed {
761 reason: String,
762 #[serde(skip_serializing_if = "Option::is_none")]
763 error_code: Option<ErrorCode>,
764 },
765 /// Successfully left spectator mode.
766 SpectatorLeft {
767 #[serde(skip_serializing_if = "Option::is_none")]
768 room_id: Option<RoomId>,
769 #[serde(skip_serializing_if = "Option::is_none")]
770 room_code: Option<String>,
771 #[serde(skip_serializing_if = "Option::is_none")]
772 reason: Option<SpectatorStateChangeReason>,
773 #[serde(default)]
774 current_spectators: Vec<SpectatorInfo>,
775 },
776 /// Another spectator joined the room.
777 NewSpectatorJoined {
778 spectator: SpectatorInfo,
779 #[serde(default)]
780 current_spectators: Vec<SpectatorInfo>,
781 #[serde(skip_serializing_if = "Option::is_none")]
782 reason: Option<SpectatorStateChangeReason>,
783 },
784 /// Another spectator left the room.
785 SpectatorDisconnected {
786 spectator_id: PlayerId,
787 #[serde(skip_serializing_if = "Option::is_none")]
788 reason: Option<SpectatorStateChangeReason>,
789 #[serde(default)]
790 current_spectators: Vec<SpectatorInfo>,
791 },
792 /// Error message.
793 Error {
794 message: String,
795 #[serde(skip_serializing_if = "Option::is_none")]
796 error_code: Option<ErrorCode>,
797 },
798 /// An opaque WebRTC signal relayed from a peer.
799 ///
800 /// **Protocol v3 only.** Sent only on a v3-negotiated connection.
801 ///
802 /// `signal` is modeled as `serde_json::Value` (typically a
803 /// [`PeerSignal`](crate::PeerSignal)) so unknown future shapes always
804 /// round-trip. Convert with `PeerSignal::try_from(&signal)`.
805 Signal {
806 /// The peer the signal came from.
807 from: PlayerId,
808 /// The opaque signal payload (offer/answer/ICE candidate).
809 signal: serde_json::Value,
810 },
811 /// A new peer to connect to after the session was finalized — a late joiner.
812 ///
813 /// **Protocol v3 only.** Sent only on a v3-negotiated connection.
814 NewPeer {
815 /// The new peer's identifier.
816 peer_id: PlayerId,
817 /// Whether the recipient sends the WebRTC offer to this peer.
818 /// Server-assigned; obey verbatim.
819 you_initiate: bool,
820 },
821 /// Per-recipient session plan for a finalized non-relay room.
822 ///
823 /// **Protocol v3 only.** Boxed to reduce enum size. May be received multiple
824 /// times (host re-election / late-join); each one fully replaces the
825 /// previous plan.
826 SessionPlan(Box<SessionPlanPayload>),
827 /// A peer's data-path transport state changed. Informational.
828 ///
829 /// **Protocol v3 only.** Sent only on a v3-negotiated connection.
830 PeerTransportStatus {
831 /// The peer whose transport state changed.
832 peer_id: PlayerId,
833 /// The transport being reported on.
834 transport: TransportKind,
835 /// Whether that transport is currently connected for the peer.
836 connected: bool,
837 },
838 /// Periodic cumulative relay-delivery diagnostics (protocol v3 only).
839 RelayStats {
840 interval_ms: u64,
841 sent_to_you: u64,
842 dropped_for_you: u64,
843 backpressure_events: u64,
844 },
845 /// Graceful server-shutdown advisory (protocol v3 only).
846 GoingAway {
847 deadline_ms: u64,
848 #[serde(default, skip_serializing_if = "Option::is_none")]
849 retry_after_secs: Option<u64>,
850 },
851 /// Exact delivery accountability (protocol v3 only).
852 DeliveryReport(Box<DeliveryReportPayload>),
853}
854
855/// Preserve omission as `None` while rejecting explicit `null`.
856fn deserialize_present_optional<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
857where
858 D: Deserializer<'de>,
859 T: Deserialize<'de>,
860{
861 T::deserialize(deserializer).map(Some)
862}
863
864/// The negotiated protocol version to restore from a reconnect's `missed_events`,
865/// if any — the last versioned (v3+) [`ProtocolInfo`](ServerMessage::ProtocolInfo)
866/// replayed in the batch.
867///
868/// A replayed v2 `ProtocolInfo` (version `None`) is ignored so it can never
869/// silently downgrade an already-negotiated v3 session; later versioned entries
870/// win over earlier ones. Shared by the async and polling clients so the two
871/// reconnect paths cannot drift apart.
872///
873/// Gated to its consumers (the async client needs `tokio-runtime`; the polling
874/// client needs `polling-client`) so it is not dead code in a build with neither.
875#[must_use]
876#[cfg(any(feature = "tokio-runtime", feature = "polling-client"))]
877pub(crate) fn replayed_negotiated_version(missed_events: &[ServerMessage]) -> Option<u16> {
878 missed_events.iter().rev().find_map(|msg| match msg {
879 ServerMessage::ProtocolInfo(info) => info.protocol_version,
880 _ => None,
881 })
882}