Events Reference¶
Decoded messages from the Signal Fish server, plus locally generated lifecycle
and diagnostic events, are surfaced as [SignalFishEvent] variants through the
event receiver returned by [SignalFishClient::start].
This page documents all 36 variants grouped by category, with field descriptions and usage examples.
Protocol v2 relay + v3 mesh
Delivery-accountability, relay diagnostics, graceful drain, and mesh events
only arrive on a v3-negotiated connection. Use
SignalFishConfig::enable_v3() for relay/accountability or
enable_mesh() when a WebRTC driver is also available.
See Protocol Versioning and the
Mesh Guide.
Type aliases used throughout
PlayerId is an alias for uuid::Uuid.
RoomId is an alias for uuid::Uuid.
Overflow backpressure and delivery boundaries
Events are not dropped merely because the bounded event channel overflows
(default 256, via SignalFishConfig::event_channel_capacity), and a
consumer that falls behind pauses the transport loop until the channel
has room — backpressure propagates to the server instead of losing
events. Inbound frames that fail to decode surface as
DecodeFailed events rather than being skipped. Delivery
can still stop or be preempted: the event receiver can be dropped,
the client handle can be dropped without calling
shutdown(), which aborts the loop immediately, or
shutdown() can abandon the one delivery currently blocked on channel
capacity. Shutdown attempts a graceful close and delivers the terminal
Disconnected best-effort; its configured deadline may abort that work.
The event channel closing is the authoritative end-of-stream signal.
Connection Events¶
Synthetic events generated by the transport layer rather than the server. Use these to track the raw connection lifecycle.
| Variant | Fields | Description |
|---|---|---|
Connected |
— | The driver first observed Transport::is_ready() == true and marked ClientSnapshot::transport_ready true; a later terminal transition resets it. Synthetic — see Connection timing for details. |
Disconnected |
reason: Option<String>, last_server_error: Option<ServerErrorInfo> |
The transport connection was closed or errored. |
DecodeFailed |
message_type: Option<String>, error: String, raw_prefix: String |
An inbound frame could not be decoded into a ServerMessage; the connection stays open. |
ProtocolViolation |
kind: ProtocolViolationKind, diagnostic: String |
A decoded message violated lifecycle, version, session-plan, signaling, or delivery-accountability invariants; configured policy decides quarantine, disconnect, or continued observation. Lifecycle/plan/signaling offenders are suppressed. |
Disconnected¶
| Field | Type | Description |
|---|---|---|
reason |
Option<String> |
Human-readable close explanation with structured transport code/reason when available. Servers use semantic codes such as 4000 server_shutdown and 4002 slow_consumer; Server 0.7 also uses 4005 room_inactive. |
last_server_error |
Option<ServerErrorInfo> |
The most recent Error/AuthenticationError received on this connection — a correlation aid for attributing the disconnect. A server that evicts a slow consumer writes a best-effort Error { error_code: SlowConsumer } farewell before closing; when that frame arrives, it shows up here. See the Delivery Contract. |
After outbound send failure, ready server errors can still populate
last_server_error; the exact bounded-drain and cause-precedence rules are in
the Delivery Contract.
Delivery of the terminal Disconnected
During normal operation Disconnected is delivered with backpressure
like every other event — it is not dropped merely because the consumer
is briefly behind. When the connection ends while a
shutdown() is pending (or the handle is dropped),
the terminal Disconnected is delivered best-effort: the transport
loop lets shutdown preempt a delivery blocked on channel capacity, and the
shutdown path attempts the terminal event with non-blocking admission. If
the channel is full, the event may be omitted. The configured shutdown
deadline may invoke the transport's abort path and stop the task before it
reaches that attempt. The event
channel closing (recv()
returns None) is the guaranteed end-of-stream signal — rely on it, not
on always observing a final Disconnected.
DecodeFailed¶
Emitted when an inbound frame fails to deserialize — an unknown message
type from a newer server, an unknown error_code string inside a known
message, a proxy injecting non-protocol frames, or corruption. The
connection stays open and later frames are unaffected; each occurrence also
increments ClientStats::messages_undecodable.
| Field | Type | Description |
|---|---|---|
message_type |
Option<String> |
The wire type tag when the frame was valid JSON. Some("Error") plus a decode failure strongly implies an unknown error_code; None means the frame was not valid JSON at all. |
error |
String |
The deserialization error text. |
raw_prefix |
String |
The raw frame, truncated to DECODE_FAILED_RAW_PREFIX_MAX (512) bytes on a UTF-8 boundary. |
Steady growth of messages_undecodable means protocol drift (upgrade this
SDK) or a corrupting middlebox — log DecodeFailed in production builds.
ProtocolViolation is distinct from DecodeFailed: its frame decoded, but
its sequence, epoch, lifecycle, gap, counter, or causal state contradicted the
negotiated protocol. Lifecycle-, plan-, and signaling-invalid messages never
mutate client state or reach the application, including under Observe.
Delivery-accountability violations retain Observe's diagnostic delivery
semantics. The default quarantine policy suppresses subsequent room game data
until an authoritative snapshot resets the baseline. See the
Delivery Contract.
match event {
SignalFishEvent::Connected => {
println!("Transport connected — waiting for authentication…");
}
SignalFishEvent::Disconnected { reason, last_server_error } => {
println!(
"Disconnected: {} (last server error: {last_server_error:?})",
reason.as_deref().unwrap_or("unknown"),
);
}
SignalFishEvent::DecodeFailed { message_type, error, .. } => {
eprintln!("undecodable frame (type {message_type:?}): {error}");
}
_ => {}
}
Authentication Events¶
Received in response to the automatic Authenticate message sent when the
client starts. You must wait for Authenticated before sending any other
commands (e.g., joining a room).
| Variant | Key Fields | Description |
|---|---|---|
Authenticated |
app_name: String, organization: Option<String>, rate_limits: RateLimitInfo |
Authentication succeeded. |
ProtocolInfo |
ProtocolInfoPayload (wrapped) |
SDK/protocol compatibility details advertised after authentication. |
AuthenticationError |
error: String, error_code: ErrorCode |
Authentication failed. |
Authenticated¶
| Field | Type | Description |
|---|---|---|
app_name |
String |
Application name confirmed by the server. |
organization |
Option<String> |
Organization the app belongs to, if any. |
rate_limits |
RateLimitInfo |
Rate limits enforced for this application (per_minute, per_hour, per_day). |
ProtocolInfo(ProtocolInfoPayload)¶
The payload is wrapped as a single struct rather than flattened. Important
fields include platform, sdk_version, capabilities, game_data_formats,
player_name_rules, the negotiated protocol version/range, and available
message transports.
AuthenticationError¶
| Field | Type | Description |
|---|---|---|
error |
String |
Human-readable error description. |
error_code |
ErrorCode |
Structured error code for programmatic handling. |
match event {
SignalFishEvent::Authenticated { app_name, rate_limits, .. } => {
println!("Authenticated as {app_name}");
println!("Rate limits: {}/min", rate_limits.per_minute);
}
SignalFishEvent::ProtocolInfo(info) => {
println!("Capabilities: {:?}", info.capabilities);
}
SignalFishEvent::AuthenticationError { error, error_code } => {
eprintln!("Auth failed [{error_code}]: {error}");
}
_ => {}
}
Room Events¶
Events related to joining, failing to join, or leaving a room.
| Variant | Key Fields | Description |
|---|---|---|
RoomJoined |
room_id, room_code, player_id, current_players, … |
Successfully joined a room. |
RoomJoinFailed |
reason: String, error_code: Option<ErrorCode> |
Failed to join a room. |
RoomLeft |
— | Successfully left the current room. |
RoomOperationFailed |
reason: String, error_code: Option<ErrorCode> |
A UUID-correlated room operation failed without an operation-specific terminal result. The exact pending fence is released. |
RoomJoined¶
| Field | Type | Description |
|---|---|---|
room_id |
RoomId |
Unique room identifier. |
room_code |
String |
Human-readable room code. |
player_id |
PlayerId |
The local player's identifier. |
game_name |
String |
Name of the game this room is for. |
max_players |
u8 |
Maximum number of players allowed. |
supports_authority |
bool |
Whether the room supports authority delegation. |
current_players |
Vec<PlayerInfo> |
Players already present in the room. |
is_authority |
bool |
Whether the local player is the authority. |
lobby_state |
LobbyState |
Current lobby readiness state (Waiting, Lobby, or Finalized). |
ready_players |
Vec<PlayerId> |
Players that have signaled readiness. |
relay_type |
String |
Legacy deployment relay label; Server 0.7 uses it as protocol metadata, not proof of a physical path. |
current_spectators |
Vec<SpectatorInfo> |
Spectators currently watching. |
ice_servers |
Vec<IceServer> |
Protocol-v3 STUN/TURN servers for early candidate gathering; empty on the v2 floor. |
reconnection_token |
Option<String> |
Server-issued v3 secret retained by ClientSnapshot for unexpected-disconnect recovery. |
RoomJoinFailed¶
| Field | Type | Description |
|---|---|---|
reason |
String |
Human-readable failure reason. |
error_code |
Option<ErrorCode> |
Structured error code, if provided. |
match event {
SignalFishEvent::RoomJoined { room_code, player_id, current_players, .. } => {
println!("Joined room {room_code} as {player_id}");
println!("{} player(s) already here", current_players.len());
}
SignalFishEvent::RoomJoinFailed { reason, error_code } => {
eprintln!("Join failed: {reason} ({error_code:?})");
}
SignalFishEvent::RoomLeft => {
println!("Left the room");
}
SignalFishEvent::RoomOperationFailed { reason, error_code } => {
eprintln!("Room operation failed: {reason} ({error_code:?})");
}
_ => {}
}
Player Events¶
Notifications about other players joining or leaving the room you are in.
| Variant | Fields | Description |
|---|---|---|
PlayerJoined |
player: PlayerInfo |
Another player joined the room. |
PlayerLeft |
player_id: PlayerId, epoch: Option<u32>, final_seq: Option<u64> |
Another player left; v3 fields identify the incarnation and terminal relay watermark. |
PlayerInfo contains id, name, is_authority, is_ready,
connected_at, optional connection_info, and optional protocol-v3 epoch
and seq snapshot metadata.
match event {
SignalFishEvent::PlayerJoined { player } => {
println!("{} joined (id: {})", player.name, player.id);
}
SignalFishEvent::PlayerLeft { player_id, .. } => {
println!("Player {player_id} left");
}
_ => {}
}
Game Data Events¶
Carry arbitrary payloads between players. JSON payloads arrive as
GameData; binary-encoded payloads (MessagePack, Rkyv) arrive as
GameDataBinary.
| Variant | Fields | Description |
|---|---|---|
GameData |
from_player, data, seq, epoch, class, key |
JSON game data plus optional v3 delivery stamp and classification. |
GameDataBinary |
from_player, encoding, payload, seq, epoch |
Binary data from a strict physical envelope; v2 has no stamps, while v3 requires both. |
GameDataEncoding is one of Json, MessagePack, or Rkyv.
Debug formatting for SignalFishEvent intentionally prints only the variant
name. Events can contain reconnect credentials and arbitrary application data;
pattern-match fields explicitly instead of logging the whole event.
match event {
SignalFishEvent::GameData { from_player, data, .. } => {
println!("JSON data from {from_player}: {data}");
}
SignalFishEvent::GameDataBinary { from_player, encoding, payload, .. } => {
println!(
"Binary data from {from_player}: {encoding:?}, {} bytes",
payload.len()
);
}
_ => {}
}
Authority Events¶
Authority delegation lets one player act as the game host. These events report changes and responses to authority requests.
| Variant | Key Fields | Description |
|---|---|---|
AuthorityChanged |
authority_player: Option<PlayerId>, you_are_authority: bool |
The room's authority assignment changed. |
AuthorityResponse |
granted: bool, reason: Option<String>, error_code: Option<ErrorCode> |
Response to an authority request. |
match event {
SignalFishEvent::AuthorityChanged { authority_player, you_are_authority } => {
if you_are_authority {
println!("You are now the authority");
} else if let Some(id) = authority_player {
println!("Authority is now player {id}");
}
}
SignalFishEvent::AuthorityResponse { granted, reason, .. } => {
if granted {
println!("Authority request granted");
} else {
println!("Authority denied: {}", reason.as_deref().unwrap_or("no reason"));
}
}
_ => {}
}
Lobby Events¶
Lobby state tracks player readiness. Readiness alone does not finalize the
lobby: after all_ready becomes true, an eligible client must call
client.start_game(). In an authority-enabled room, only the current authority
is eligible. A successful request produces GameStarting with peer connection
details.
| Variant | Key Fields | Description |
|---|---|---|
LobbyStateChanged |
lobby_state: LobbyState, ready_players: Vec<PlayerId>, all_ready: bool |
The lobby readiness state changed. |
GameStarting |
peer_connections: Vec<PeerConnectionInfo> |
The game is starting with peer connection info. |
LobbyState is one of Waiting, Lobby, or Finalized.
PeerConnectionInfo contains player_id, player_name, is_authority,
relay_type, and an optional connection_info.
match event {
SignalFishEvent::LobbyStateChanged { lobby_state, ready_players, all_ready } => {
println!("Lobby: {lobby_state:?}, {}/{} ready",
ready_players.len(),
ready_players.len() + if all_ready { 0 } else { 1 }
);
}
SignalFishEvent::GameStarting { peer_connections } => {
println!("Game starting with {} peers", peer_connections.len());
for peer in &peer_connections {
println!(" {} (authority={})", peer.player_name, peer.is_authority);
}
}
_ => {}
}
Do not call start_game() on every repeated ready-state update. Keep a
one-shot request latch, and in authority-enabled rooms re-evaluate eligibility
when AuthorityChanged arrives. The compiling
basic_lobby example
shows that complete pattern.
Mesh Events (protocol v3)¶
v3-negotiated connections only
These four events arrive only when the connection has negotiated protocol v3 and the server emits the corresponding plan/signaling state. A v2 relay-floor connection never emits them. See the Mesh Guide for the full peer-to-peer flow and Protocol Versioning for how negotiation works.
These events carry the server's WebRTC mesh signaling. The server is
authoritative: it chooses the topology and assigns the deterministic WebRTC
offerer via the initiate / you_initiate flags, which you must obey verbatim
(never compute who offers — that avoids WebRTC glare).
| Variant | Key Fields | Description |
|---|---|---|
SessionPlan |
generation, topology, transport, host, direct_endpoint, peers, ice_servers, fallback |
The server's per-recipient authoritative session plan. |
NewPeer |
peer_id: PlayerId, you_initiate: bool |
A late-joining peer to connect to after the session was finalized. |
SignalReceived |
from: PlayerId, generation, signal: serde_json::Value |
A generation-validated opaque WebRTC signal relayed from a peer. |
PeerTransportStatus |
peer_id: PlayerId, transport: TransportKind, connected: bool |
A peer's data-path transport state changed (informational). |
SessionPlan¶
May arrive multiple times (host re-election, late-join re-plan); each one fully replaces the previous plan — replace the peer set, never merge.
| Field | Type | Description |
|---|---|---|
generation |
Option<SessionGeneration> |
Handshake generation. Some on server 0.7; None only for legacy generation-less plans. |
topology |
Topology |
Chosen session topology (Relay, Host, or Mesh). |
transport |
TransportKind |
Chosen data-path transport (Relay, Direct, or WebRtc). |
host |
Option<PlayerId> |
The elected host (present for Host topology). |
direct_endpoint |
Option<DirectEndpoint> |
Validated endpoint for Host + Direct; opening the socket remains the application's responsibility. |
peers |
Vec<SessionPeer> |
Peers this client should connect to, each with its server-assigned initiate flag. |
ice_servers |
Vec<IceServer> |
ICE (STUN/TURN) servers for WebRTC. |
fallback |
TransportKind |
The universal fallback transport (always Relay). |
SignalReceived¶
Convert the opaque signal value with PeerSignal::try_from(&signal) for the
common Offer / Answer / IceCandidate shapes; the raw Value is preserved
for any other shape.
use std::collections::HashSet;
use signal_fish_client::{PeerSignal, PlayerId, TransportKind};
let mut current_generation = None;
let mut webrtc_plan_active = false;
let mut connected_peers = HashSet::<PlayerId>::new();
match event {
SignalFishEvent::SessionPlan {
generation, topology, transport, peers, ice_servers, ..
} => {
// Every plan replaces the previous physical graph, including Direct
// and Relay reset plans.
for peer in connected_peers.drain() {
my_driver.disconnect(peer);
}
webrtc_plan_active = transport == TransportKind::WebRtc;
current_generation = webrtc_plan_active.then_some(generation).flatten();
println!("Session plan: {topology:?} with {} peer(s)", peers.len());
if webrtc_plan_active {
my_driver.set_ice_servers(&ice_servers);
for peer in &peers {
connected_peers.insert(peer.player_id);
my_driver.connect(peer.player_id, generation, peer.initiate);
}
}
}
SignalFishEvent::NewPeer { peer_id, you_initiate } if webrtc_plan_active => {
connected_peers.insert(peer_id);
my_driver.connect(peer_id, current_generation, you_initiate);
}
SignalFishEvent::SignalReceived { from, generation, signal } => {
if webrtc_plan_active
&& generation == current_generation
&& connected_peers.contains(&from)
{
if let Ok(peer_signal) = PeerSignal::try_from(&signal) {
my_driver.on_signal(from, generation, peer_signal);
}
}
}
SignalFishEvent::PeerTransportStatus { peer_id, connected, .. } => {
println!("Peer {peer_id} transport connected={connected}");
}
_ => {}
}
Let the SDK do the choreography
With the mesh feature, MeshController
handles all four of these events for you — calling your WebRtcDriver,
relaying signals, and reporting transport status — so you rarely match them
by hand. See the Mesh Guide.
Delivery and Drain Events (protocol v3)¶
| Variant | Fields | Description |
|---|---|---|
DeliveryReport(payload) |
DeliveryReportPayload |
Cumulative per-class outcomes and exact omitted sequence ranges. Exact ranges are the only authorization for continuing-connection gaps. |
RelayStats |
interval_ms, sent_to_you, dropped_for_you, backpressure_events |
Optional cumulative connection diagnostics; never gap authorization. |
GoingAway |
deadline_ms, retry_after_secs |
Best-effort graceful-drain advisory. The subsequent structured transport close remains authoritative. |
Applications normally let the SDK consume these for accountability and also
record the typed events for telemetry. GoingAway is a cue to preserve the
current reconnect snapshot and prepare a retry after the advertised delay.
Heartbeat Events¶
Call client.ping() to send a heartbeat message that keeps the connection alive.
The server replies with a Pong event confirming receipt.
| Variant | Fields | Description |
|---|---|---|
Pong |
— | Connection-scoped response to a ping; valid while authentication or protocol negotiation is still in flight. |
match event {
SignalFishEvent::Pong => {
// Connection is healthy — usually no action needed.
}
_ => {}
}
Reconnection Events¶
If a player's connection drops, the SDK can attempt to rejoin the same room. On success the server replays any events that were missed.
| Variant | Key Fields | Description |
|---|---|---|
Reconnected |
room_id, room_code, player_id, missed_events, … |
Reconnection succeeded; state is restored. |
ReconnectionFailed |
reason: String, error_code: ErrorCode |
Reconnection failed. |
PlayerReconnected |
player_id: PlayerId, epoch: Option<u32> |
Another player reconnected; v3 carries the new incarnation epoch. |
Reconnected¶
Carries the same room-state fields as RoomJoined plus:
| Field | Type | Description |
|---|---|---|
missed_events |
Vec<SignalFishEvent> |
Events that occurred while the client was disconnected. |
replay |
Option<ReplayStatus> |
Whether the replayed control-event suffix is complete or truncated. |
sender_watermarks |
Vec<SenderWatermark> |
Authoritative per-sender epoch/sequence baselines for resumed delivery. |
ice_servers |
Vec<IceServer> |
Protocol-v3 STUN/TURN servers for early candidate gathering. |
reconnection_token |
Option<String> |
Fresh secret replacing the consumed token; also stored in ClientSnapshot. |
ReconnectionFailed¶
| Field | Type | Description |
|---|---|---|
reason |
String |
Human-readable failure reason. |
error_code |
ErrorCode |
Structured error code. |
match event {
SignalFishEvent::Reconnected { room_code, missed_events, .. } => {
println!("Reconnected to {room_code}");
println!("Replaying {} missed events", missed_events.len());
for missed in &missed_events {
println!(" missed: {missed:?}");
}
}
SignalFishEvent::ReconnectionFailed { reason, error_code } => {
eprintln!("Reconnection failed [{error_code}]: {reason}");
}
SignalFishEvent::PlayerReconnected { player_id, .. } => {
println!("Player {player_id} reconnected");
}
_ => {}
}
Spectator Events¶
Spectators can watch a room without participating. These events cover the full spectator lifecycle.
| Variant | Key Fields | Description |
|---|---|---|
SpectatorJoined |
room_id, spectator_id, current_players, current_spectators, … |
Successfully joined a room as a spectator. |
SpectatorJoinFailed |
reason: String, error_code: Option<ErrorCode> |
Failed to join as a spectator. |
SpectatorLeft |
room_id: Option<RoomId>, room_code: Option<String>, reason, current_spectators |
Successfully left spectator mode. |
NewSpectatorJoined |
spectator: SpectatorInfo, current_spectators, reason |
Another spectator joined the room. |
SpectatorDisconnected |
spectator_id: PlayerId, reason, current_spectators |
Another spectator disconnected. |
SpectatorJoined¶
| Field | Type | Description |
|---|---|---|
room_id |
RoomId |
Unique room identifier. |
room_code |
String |
Human-readable room code. |
spectator_id |
PlayerId |
The local spectator's identifier. |
game_name |
String |
Name of the game this room is for. |
current_players |
Vec<PlayerInfo> |
Players currently in the room. |
current_spectators |
Vec<SpectatorInfo> |
Spectators currently watching. |
lobby_state |
LobbyState |
Current lobby readiness state. |
reason |
Option<SpectatorStateChangeReason> |
Reason the spectator state changed, if applicable. |
SpectatorLeft¶
| Field | Type | Description |
|---|---|---|
room_id |
Option<RoomId> |
Room identifier, if available. |
room_code |
Option<String> |
Room code, if available. |
reason |
Option<SpectatorStateChangeReason> |
Reason for leaving, if available. |
current_spectators |
Vec<SpectatorInfo> |
Remaining spectators in the room. |
SpectatorStateChangeReason is one of Joined, VoluntaryLeave,
Disconnected, Removed, or RoomClosed.
match event {
SignalFishEvent::SpectatorJoined { room_code, spectator_id, current_players, .. } => {
println!("Spectating room {room_code} as {spectator_id}");
println!("{} player(s) in game", current_players.len());
}
SignalFishEvent::SpectatorJoinFailed { reason, error_code } => {
eprintln!("Spectator join failed: {reason} ({error_code:?})");
}
SignalFishEvent::SpectatorLeft { reason, .. } => {
println!("Left spectator mode: {reason:?}");
}
SignalFishEvent::NewSpectatorJoined { spectator, current_spectators, .. } => {
println!("{} started spectating ({} total)",
spectator.name, current_spectators.len());
}
SignalFishEvent::SpectatorDisconnected { spectator_id, current_spectators, .. } => {
println!("Spectator {spectator_id} left ({} remaining)",
current_spectators.len());
}
_ => {}
}
Error Events¶
Catch-all for server-side errors that don't fit a more specific variant.
| Variant | Fields | Description |
|---|---|---|
Error |
message: String, error_code: Option<ErrorCode> |
A generic server error. |
match event {
SignalFishEvent::Error { message, error_code } => {
eprintln!("Server error: {message} ({error_code:?})");
}
_ => {}
}
Complete Event Loop Pattern¶
A realistic event loop handling the most important variants. Use this as a starting template and expand as needed for your game.
use signal_fish_client::{
JoinRoomParams, SignalFishClient, SignalFishConfig,
SignalFishEvent, WebSocketTransport,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = std::env::var("SIGNAL_FISH_URL")
.unwrap_or_else(|_| "ws://localhost:3536/v2/ws".to_string());
let transport = WebSocketTransport::connect(&url).await?;
let config = SignalFishConfig::new("your-app-id");
let (mut client, mut event_rx) = SignalFishClient::start(transport, config);
let mut start_request_sent = false;
while let Some(event) = event_rx.recv().await {
match event {
// ── Connection ──────────────────────────────────────
SignalFishEvent::Connected => {
println!("Connected — authenticating…");
}
// ── Authentication ──────────────────────────────────
SignalFishEvent::Authenticated { app_name, .. } => {
println!("Authenticated as {app_name}");
client.join_room(JoinRoomParams::new("my-game", "Alice"))?;
}
SignalFishEvent::AuthenticationError { error, error_code } => {
eprintln!("Auth failed [{error_code}]: {error}");
break;
}
// ── Room lifecycle ──────────────────────────────────
SignalFishEvent::RoomJoined { room_code, current_players, .. } => {
println!("Joined room {room_code}");
println!("{} player(s) in room", current_players.len());
client.set_ready()?;
}
// ── Player presence ─────────────────────────────────
SignalFishEvent::PlayerJoined { player } => {
println!("{} joined the room", player.name);
}
SignalFishEvent::PlayerLeft { player_id, .. } => {
println!("Player {player_id} left");
}
// ── Game data ───────────────────────────────────────
SignalFishEvent::GameData { from_player, data, .. } => {
println!("Data from {from_player}: {data}");
}
// ── Lobby ───────────────────────────────────────────
SignalFishEvent::LobbyStateChanged { all_ready, .. } => {
if all_ready && !start_request_sent {
println!("All players ready!");
// This example creates a non-authority room. Authority-enabled
// rooms must additionally require that this client is authority.
client.start_game()?;
start_request_sent = true;
}
}
SignalFishEvent::GameStarting { peer_connections } => {
println!("Game starting with {} peers!", peer_connections.len());
}
// ── Errors ──────────────────────────────────────────
SignalFishEvent::Error { message, error_code } => {
eprintln!("Server error: {message} ({error_code:?})");
}
// ── Disconnection ───────────────────────────────────
SignalFishEvent::Disconnected { reason, .. } => {
println!("Disconnected: {}",
reason.as_deref().unwrap_or("unknown"));
break;
}
// ── Everything else ─────────────────────────────────
_ => {}
}
}
client.shutdown().await;
Ok(())
}
Tip
All public enums in this crate are exhaustive. This introductory loop uses
_ => {} to focus on common events. Production code can enumerate every
variant and omit the catch-all when it wants compiler errors to surface new
variants during a breaking-version upgrade.