Skip to main content

audio_code/net/
mod.rs

1//! Online play: host a game or join one over the internet or the local network.
2//! Available as `ctx.net`.
3//!
4//! One player (or a dedicated server, see [`Engine::run_server`](crate::Engine::run_server))
5//! hosts, the others join. Everyone sends [`Packet`]s: a kind and a few values. What
6//! arrives shows up as [`NetEvent`]s for one frame, like key presses.
7//!
8//! ```no_run
9//! use audio_code::prelude::*;
10//!
11//! fn main() -> audio_code::Result<()> {
12//!     audio_code::run("Chat", |ctx| {
13//!         if ctx.once("start") {
14//!             ctx.net.set_name("Anna");
15//!             ctx.net.join("localhost"); // or: ctx.net.host(net::DEFAULT_PORT)
16//!         }
17//!         for player in ctx.net.joined() {
18//!             ctx.speak(format!("Connected to {}", ctx.net.name(player)));
19//!         }
20//!         if ctx.pressed(Key::Space) {
21//!             ctx.net.send_all(Packet::new("chat").with("Hello!"));
22//!         }
23//!         for (from, packet) in ctx.net.received("chat") {
24//!             ctx.speak(format!("{}: {}", ctx.net.name(from), packet.text(0)));
25//!         }
26//!         for (_, reason) in ctx.net.left() {
27//!             ctx.speak(ctx.lang.describe_reason(&reason));
28//!         }
29//!     })
30//! }
31//! ```
32//!
33//! ## What the engine does for you
34//!
35//! * Connecting, the handshake and all sending and receiving run on background threads.
36//!   The game never waits for the network.
37//! * Only clients of the same game (the [`Engine`](crate::Engine) title) and the same
38//!   version ([`Net::set_version`]) get in. Others are turned away with
39//!   [`Reason::WrongGame`] / [`Reason::WrongVersion`].
40//! * Both sides ping each other every second ([`Net::ping`]). A connection that hears
41//!   nothing for 10 seconds is closed with [`Reason::TimedOut`].
42//! * Packets arrive complete, unchanged and in the order they were sent.
43//! * Mistakes do not crash: sending while offline does nothing, joining a wrong address
44//!   ends with a [`NetEvent::Disconnected`] that says why.
45//!
46//! ## WebSockets and encryption
47//!
48//! Connections are WebSockets, so they pass firewalls and web proxies that only let web
49//! traffic through, and a server can sit behind a web server such as nginx or Caddy.
50//! [`Net::join`] takes plain addresses (`"example.com"`, `"10.0.0.2:7777"`) as well as
51//! `ws://` and `wss://` URLs.
52//!
53//! Everything is **always encrypted**, without any setup: both sides make fresh keys for
54//! every connection (X25519) and encrypt every message (XChaCha20-Poly1305). Nobody who
55//! listens in can read names, chat or game data, and changed messages are detected. This
56//! does not prove *who* the server is, though: someone who can redirect the traffic could
57//! pose as it. To rule that out, put the server behind a web server with a real TLS
58//! certificate and join with `wss://` (feature `tls`, on by default).
59
60mod crypto;
61mod packet;
62mod wire;
63
64use std::collections::BTreeMap;
65use std::fmt;
66use std::net::{IpAddr, TcpListener, UdpSocket};
67use std::sync::atomic::{AtomicBool, Ordering};
68use std::sync::mpsc::{Receiver, Sender};
69use std::sync::{Arc, mpsc};
70
71pub use packet::{Arg, Packet};
72
73use crate::error::{Error, Result};
74use wire::{Event, Frame, Link};
75
76/// The port used when none is given: `ctx.net.join("example.com")` connects to it.
77pub const DEFAULT_PORT: u16 = 7777;
78
79/// Names are cut to this many characters.
80const MAX_NAME: usize = 32;
81
82/// A connected player. On a client the server is [`PeerId::SERVER`].
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
84pub struct PeerId(u32);
85
86impl PeerId {
87    /// The host. Clients receive everything from this peer. The host itself has this id too.
88    pub const SERVER: PeerId = PeerId(0);
89
90    /// The number behind the id. The server is 0, clients are numbered from 1 in the order
91    /// they joined. Numbers are not reused while the server runs.
92    pub fn number(self) -> u32 {
93        self.0
94    }
95
96    /// The id with this number, e.g. one read from a packet with [`Packet::peer`].
97    pub fn from_number(number: u32) -> Self {
98        Self(number)
99    }
100
101    /// Whether this is [`PeerId::SERVER`].
102    pub fn is_server(self) -> bool {
103        self == Self::SERVER
104    }
105}
106
107impl fmt::Display for PeerId {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        write!(f, "{}", self.0)
110    }
111}
112
113/// Why a connection ended. Speak it with `ctx.lang.describe_reason(&reason)`.
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub enum Reason {
116    /// The player left on purpose ([`Net::close`] or quitting the game).
117    Left,
118    /// The host closed the server.
119    Closed,
120    /// The host kicked this player, with the text given to [`Net::kick`].
121    Kicked(String),
122    /// The server already has the maximum number of players ([`Net::set_max_players`]).
123    ServerFull,
124    /// The server runs another game; contains its title.
125    WrongGame(String),
126    /// The server runs another version of the game; contains its version.
127    WrongVersion(String),
128    /// Nothing was heard from the other side for 10 seconds.
129    TimedOut,
130    /// Joining failed: wrong address, no server running there, no network.
131    Failed(String),
132    /// The connection broke; contains the system's description.
133    Lost(String),
134}
135
136impl fmt::Display for Reason {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        f.write_str(&crate::lang::Lang::new().describe_reason(self))
139    }
140}
141
142/// Something that happened on the network this frame.
143#[derive(Debug, Clone, PartialEq)]
144pub enum NetEvent {
145    /// Host: a player joined. Client: the connection to the server is ready
146    /// (the peer is [`PeerId::SERVER`]).
147    Connected(PeerId),
148    /// A packet arrived.
149    Message(PeerId, Packet),
150    /// Host: a player left. Client: the connection to the server ended or could not be made.
151    Disconnected(PeerId, Reason),
152}
153
154enum State {
155    Offline,
156    Hosting { port: u16, stop: Arc<AtomicBool> },
157    Joining,
158    Joined,
159}
160
161struct Peer {
162    name: String,
163    link: Link,
164    ping: f32,
165}
166
167/// The network connection: `ctx.net`. See the [module docs](self).
168pub struct Net {
169    state: State,
170    events_rx: Option<Receiver<Event>>,
171    peers: BTreeMap<PeerId, Peer>,
172    pub(crate) events: Vec<NetEvent>,
173    /// Events made by the game itself (kick, failed start), shown next frame.
174    pending: Vec<NetEvent>,
175    game: String,
176    version: String,
177    name: String,
178    max_players: usize,
179    id: PeerId,
180    warned_offline: bool,
181}
182
183impl fmt::Debug for Net {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        let state = match self.state {
186            State::Offline => "offline",
187            State::Hosting { .. } => "hosting",
188            State::Joining => "joining",
189            State::Joined => "joined",
190        };
191        f.debug_struct("Net").field("state", &state).field("players", &self.peers.len()).finish_non_exhaustive()
192    }
193}
194
195impl Net {
196    /// An offline network for the game `game`. Only clients with the same game name can join.
197    /// The engine creates this as `ctx.net` with the game title.
198    pub fn new(game: impl Into<String>) -> Self {
199        Self {
200            state: State::Offline,
201            events_rx: None,
202            peers: BTreeMap::new(),
203            events: Vec::new(),
204            pending: Vec::new(),
205            game: game.into(),
206            version: String::new(),
207            name: "Player".into(),
208            max_players: 32,
209            id: PeerId::SERVER,
210            warned_offline: false,
211        }
212    }
213
214    /// The name others see (default `"Player"`, at most 32 characters). Set it before
215    /// [`Net::host`] or [`Net::join`].
216    pub fn set_name(&mut self, name: impl Into<String>) {
217        self.name = clean_name(&name.into());
218    }
219
220    /// The version of your game, e.g. `"1.2"` (default empty). Clients with another version
221    /// cannot join, so old and new game code never meet. Set it before hosting or joining.
222    pub fn set_version(&mut self, version: impl Into<String>) {
223        self.version = version.into();
224    }
225
226    /// How many clients may join (default 32). The host does not count.
227    pub fn set_max_players(&mut self, max: usize) {
228        self.max_players = max;
229    }
230
231    /// Starts a WebSocket server on `port` that others can join. Port 0 picks a free port
232    /// (see [`Net::port`]). Ends any connection that is open. Fails when the port is in use.
233    /// Any URL path is accepted, so a web server can pass on `wss://example.com/game`.
234    ///
235    /// Players outside your local network need the host's public address, and the port
236    /// must be forwarded in the host's router.
237    pub fn host(&mut self, port: u16) -> Result<()> {
238        self.close();
239        let listener = TcpListener::bind(("0.0.0.0", port))
240            .map_err(|e| Error::Net(format!("could not host on port {port}: {e}")))?;
241        let port = listener.local_addr().map(|a| a.port()).unwrap_or(port);
242        listener.set_nonblocking(true)?;
243        let stop = Arc::new(AtomicBool::new(false));
244        let tx = self.new_channel();
245        let flag = stop.clone();
246        std::thread::Builder::new()
247            .name("audio-code net server".into())
248            .spawn(move || wire::accept_loop(listener, flag, tx))?;
249        self.state = State::Hosting { port, stop };
250        self.id = PeerId::SERVER;
251        Ok(())
252    }
253
254    /// Joins the server at `address`: a host name or IP address, with or without port
255    /// (`"example.com"`, `"192.168.1.20:7777"`, `"[::1]:7777"`), or a WebSocket URL
256    /// (`"ws://example.com:7777/"`, `"wss://example.com/game"`). Without a port,
257    /// [`DEFAULT_PORT`] is used (URLs: 80 for `ws://`, 443 for `wss://`). Ends any connection
258    /// that is open.
259    ///
260    /// Returns at once. Later a [`NetEvent::Connected`] says the connection is ready, or a
261    /// [`NetEvent::Disconnected`] says why it failed (after at most about 10 seconds).
262    pub fn join(&mut self, address: &str) {
263        self.close();
264        let tx = self.new_channel();
265        let hello = Frame::Hello {
266            protocol: wire::PROTOCOL,
267            game: self.game.clone(),
268            version: self.version.clone(),
269            name: self.name.clone(),
270        };
271        let address = address.to_string();
272        let spawned = std::thread::Builder::new()
273            .name("audio-code net client".into())
274            .spawn(move || wire::connect(address, hello, tx));
275        if let Err(e) = spawned {
276            self.pending.push(NetEvent::Disconnected(PeerId::SERVER, Reason::Failed(e.to_string())));
277            return;
278        }
279        self.state = State::Joining;
280    }
281
282    /// Leaves the game, or stops the server and sends every player away with
283    /// [`Reason::Closed`]. Called automatically when the game quits. Does nothing when
284    /// offline. Produces no events.
285    pub fn close(&mut self) {
286        let bye = if self.is_host() { Reason::Closed } else { Reason::Left };
287        for peer in std::mem::take(&mut self.peers).into_values() {
288            peer.link.close(Some(bye.clone()));
289        }
290        if let State::Hosting { stop, .. } = &self.state {
291            stop.store(true, Ordering::Relaxed);
292        }
293        self.state = State::Offline;
294        self.events_rx = None;
295        self.id = PeerId::SERVER;
296        self.warned_offline = false;
297    }
298
299    /// Host: sends a player away. They get [`Reason::Kicked`] with `reason` (may be empty).
300    /// The host gets a [`NetEvent::Disconnected`] for them next frame.
301    pub fn kick(&mut self, peer: PeerId, reason: &str) {
302        if !self.is_host() {
303            return;
304        }
305        if let Some(p) = self.peers.remove(&peer) {
306            p.link.close(Some(Reason::Kicked(reason.to_string())));
307            self.pending.push(NetEvent::Disconnected(peer, Reason::Kicked(reason.to_string())));
308        }
309    }
310
311    /// Sends a packet to one peer. On a client, the only peer is [`PeerId::SERVER`]. Does
312    /// nothing if the peer is not connected.
313    pub fn send(&mut self, peer: PeerId, packet: impl Into<Packet>) {
314        let bytes = encode(packet.into());
315        match self.peers.get(&peer) {
316            Some(p) => p.link.send(bytes),
317            None => self.warn_offline(),
318        }
319    }
320
321    /// Sends a packet to every connected peer. On a client that is the server.
322    pub fn send_all(&mut self, packet: impl Into<Packet>) {
323        self.send_filtered(packet.into(), |_| true);
324    }
325
326    /// Sends a packet to every connected peer except one. The usual way for a host to pass
327    /// on what one player did to everybody else.
328    pub fn send_all_except(&mut self, except: PeerId, packet: impl Into<Packet>) {
329        self.send_filtered(packet.into(), |p| p != except);
330    }
331
332    fn send_filtered(&mut self, packet: Packet, keep: impl Fn(PeerId) -> bool) {
333        if self.peers.is_empty() {
334            if !self.is_host() {
335                self.warn_offline();
336            }
337            return;
338        }
339        let bytes = encode(packet);
340        for (_, p) in self.peers.iter().filter(|(id, _)| keep(**id)) {
341            p.link.send(bytes.clone());
342        }
343    }
344
345    fn warn_offline(&mut self) {
346        if !self.warned_offline && !matches!(self.state, State::Joined | State::Hosting { .. }) {
347            self.warned_offline = true;
348            eprintln!("audio-code: a packet was sent while not connected; it was dropped");
349        }
350    }
351
352    /// Everything that happened on the network this frame, in order.
353    pub fn events(&self) -> &[NetEvent] {
354        &self.events
355    }
356
357    /// Players who connected this frame. On a client: [`PeerId::SERVER`] when the
358    /// connection became ready.
359    pub fn joined(&self) -> Vec<PeerId> {
360        self.events
361            .iter()
362            .filter_map(|e| match e {
363                NetEvent::Connected(p) => Some(*p),
364                _ => None,
365            })
366            .collect()
367    }
368
369    /// Players who left this frame and why. On a client: [`PeerId::SERVER`] when the
370    /// connection ended or could not be made.
371    pub fn left(&self) -> Vec<(PeerId, Reason)> {
372        self.events
373            .iter()
374            .filter_map(|e| match e {
375                NetEvent::Disconnected(p, r) => Some((*p, r.clone())),
376                _ => None,
377            })
378            .collect()
379    }
380
381    /// All packets that arrived this frame, with their sender.
382    pub fn messages(&self) -> Vec<(PeerId, Packet)> {
383        self.events
384            .iter()
385            .filter_map(|e| match e {
386                NetEvent::Message(p, packet) => Some((*p, packet.clone())),
387                _ => None,
388            })
389            .collect()
390    }
391
392    /// The packets of one kind that arrived this frame, with their sender.
393    pub fn received(&self, kind: &str) -> Vec<(PeerId, Packet)> {
394        self.events
395            .iter()
396            .filter_map(|e| match e {
397                NetEvent::Message(p, packet) if packet.is(kind) => Some((*p, packet.clone())),
398                _ => None,
399            })
400            .collect()
401    }
402
403    /// The connected peers in the order they joined. Host: all clients. Client: the server,
404    /// once connected.
405    pub fn players(&self) -> Vec<PeerId> {
406        self.peers.keys().copied().collect()
407    }
408
409    /// Number of connected peers (the host does not count itself).
410    pub fn player_count(&self) -> usize {
411        self.peers.len()
412    }
413
414    /// Whether `peer` is connected right now.
415    pub fn is_connected_to(&self, peer: PeerId) -> bool {
416        self.peers.contains_key(&peer)
417    }
418
419    /// The name a peer chose with [`Net::set_name`], or `""` if not connected. Your own
420    /// [`Net::id`] gives your own name.
421    pub fn name(&self, peer: PeerId) -> &str {
422        if peer == self.id && !matches!(self.state, State::Offline | State::Joining) {
423            return &self.name;
424        }
425        self.peers.get(&peer).map_or("", |p| p.name.as_str())
426    }
427
428    /// The round trip time to a peer in seconds: how long a message takes there and back.
429    /// Updated every second; 0.0 until measured or when not connected.
430    pub fn ping(&self, peer: PeerId) -> f32 {
431        self.peers.get(&peer).map_or(0.0, |p| p.ping)
432    }
433
434    /// Your own id: [`PeerId::SERVER`] for the host, the number the server gave you on a
435    /// client.
436    pub fn id(&self) -> PeerId {
437        self.id
438    }
439
440    /// Whether this game is hosting.
441    pub fn is_host(&self) -> bool {
442        matches!(self.state, State::Hosting { .. })
443    }
444
445    /// Whether this game is a client that is connected to a server.
446    pub fn is_client(&self) -> bool {
447        matches!(self.state, State::Joined)
448    }
449
450    /// Whether a [`Net::join`] is still in progress.
451    pub fn is_joining(&self) -> bool {
452        matches!(self.state, State::Joining)
453    }
454
455    /// Whether this game is hosting or connected to a server.
456    pub fn is_online(&self) -> bool {
457        self.is_host() || self.is_client()
458    }
459
460    /// The port the server listens on while hosting.
461    pub fn port(&self) -> Option<u16> {
462        match self.state {
463            State::Hosting { port, .. } => Some(port),
464            _ => None,
465        }
466    }
467
468    /// This computer's address in the local network, e.g. `192.168.1.20`: what players in
469    /// the same network type to join. `None` without a network. Sends nothing.
470    pub fn local_ip() -> Option<IpAddr> {
471        let socket = UdpSocket::bind("0.0.0.0:0").ok()?;
472        // Connecting a UDP socket only picks the outgoing interface.
473        socket.connect("8.8.8.8:80").ok()?;
474        socket.local_addr().ok().map(|a| a.ip()).filter(|ip| !ip.is_unspecified())
475    }
476
477    fn new_channel(&mut self) -> Sender<Event> {
478        let (tx, rx) = mpsc::channel();
479        self.events_rx = Some(rx);
480        tx
481    }
482
483    /// Collects what the background threads reported. Called by the engine every frame.
484    pub(crate) fn update(&mut self) {
485        self.events = std::mem::take(&mut self.pending);
486        let Some(rx) = &self.events_rx else {
487            return;
488        };
489        let reports: Vec<Event> = rx.try_iter().collect();
490        for report in reports {
491            self.handle(report);
492        }
493    }
494
495    fn handle(&mut self, report: Event) {
496        match report {
497            Event::Hello { link, protocol, game, version, name } => {
498                if !self.is_host() {
499                    link.close(None);
500                    return;
501                }
502                let refusal = if game != self.game {
503                    Some(Reason::WrongGame(self.game.clone()))
504                } else if version != self.version || protocol != wire::PROTOCOL {
505                    Some(Reason::WrongVersion(self.version.clone()))
506                } else if self.peers.len() >= self.max_players {
507                    Some(Reason::ServerFull)
508                } else {
509                    None
510                };
511                if let Some(reason) = refusal {
512                    link.close(Some(reason));
513                    return;
514                }
515                let peer = PeerId(link.id);
516                link.send_frame(&Frame::Welcome { id: link.id, name: self.name.clone() });
517                self.peers.insert(peer, Peer { name: clean_name(&name), link, ping: 0.0 });
518                self.events.push(NetEvent::Connected(peer));
519            }
520            Event::Welcome { link, id, name } => {
521                self.state = State::Joined;
522                self.id = PeerId(id);
523                self.peers.insert(PeerId::SERVER, Peer { name: clean_name(&name), link, ping: 0.0 });
524                self.events.push(NetEvent::Connected(PeerId::SERVER));
525            }
526            Event::Data { peer, packet } => {
527                let peer = PeerId(peer);
528                if self.peers.contains_key(&peer) {
529                    self.events.push(NetEvent::Message(peer, packet));
530                }
531            }
532            Event::Ping { peer, seconds } => {
533                if let Some(p) = self.peers.get_mut(&PeerId(peer)) {
534                    p.ping = seconds;
535                }
536            }
537            Event::Closed { peer, reason } => {
538                let peer = PeerId(peer);
539                let known = self.peers.remove(&peer).is_some();
540                let joining = self.is_joining() && peer == PeerId::SERVER;
541                if known || joining {
542                    self.events.push(NetEvent::Disconnected(peer, reason));
543                }
544                if !self.is_host() && peer == PeerId::SERVER {
545                    self.state = State::Offline;
546                    self.events_rx = None;
547                    self.id = PeerId::SERVER;
548                }
549            }
550        }
551    }
552}
553
554impl Drop for Net {
555    fn drop(&mut self) {
556        self.close();
557    }
558}
559
560fn encode(packet: Packet) -> Arc<[u8]> {
561    let bytes = Frame::Data(packet).encode();
562    if bytes.len() > wire::MAX_FRAME {
563        eprintln!("audio-code: a packet of {} bytes is too large to send (at most 1 MB); it was dropped", bytes.len());
564        return Arc::from(&[][..]);
565    }
566    bytes.into()
567}
568
569fn clean_name(name: &str) -> String {
570    name.chars().filter(|c| !c.is_control()).take(MAX_NAME).collect::<String>().trim().to_string()
571}
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576    use std::time::{Duration, Instant};
577
578    /// Updates all nets until `done` holds or eight seconds pass (Windows takes two seconds
579    /// to report a refused connection).
580    fn pump(nets: &mut [&mut Net], mut done: impl FnMut(&[&mut Net]) -> bool) -> Vec<Vec<NetEvent>> {
581        let mut all = vec![Vec::new(); nets.len()];
582        let start = Instant::now();
583        while start.elapsed() < Duration::from_secs(8) {
584            for (net, events) in nets.iter_mut().zip(&mut all) {
585                net.update();
586                events.extend(net.events.iter().cloned());
587            }
588            if done(nets) {
589                break;
590            }
591            std::thread::sleep(Duration::from_millis(5));
592        }
593        all
594    }
595
596    fn host() -> (Net, String) {
597        let mut server = Net::new("game");
598        server.set_name("Host");
599        server.host(0).unwrap();
600        let address = format!("127.0.0.1:{}", server.port().unwrap());
601        (server, address)
602    }
603
604    #[test]
605    fn join_chat_and_leave() {
606        let (mut server, address) = host();
607        let mut client = Net::new("game");
608        client.set_name("Anna");
609        client.join(&address);
610        pump(&mut [&mut server, &mut client], |n| n[0].player_count() == 1 && n[1].is_client());
611        let anna = server.players()[0];
612        assert_eq!(server.name(anna), "Anna");
613        assert_eq!(client.name(PeerId::SERVER), "Host");
614        assert_eq!(client.id(), anna);
615        assert_eq!(client.name(client.id()), "Anna");
616
617        client.send_all(Packet::new("chat").with("hi"));
618        let events = pump(&mut [&mut server, &mut client], |n| !n[0].received("chat").is_empty());
619        assert_eq!(events[0], [NetEvent::Message(anna, Packet::new("chat").with("hi"))]);
620
621        server.send(anna, "pong");
622        let events = pump(&mut [&mut server, &mut client], |n| !n[1].messages().is_empty());
623        assert_eq!(events[1], [NetEvent::Message(PeerId::SERVER, Packet::new("pong"))]);
624
625        client.close();
626        let events = pump(&mut [&mut server], |n| n[0].player_count() == 0);
627        assert_eq!(events[0], [NetEvent::Disconnected(anna, Reason::Left)]);
628    }
629
630    #[test]
631    fn refuses_wrong_game_version_and_full() {
632        let (mut server, address) = host();
633        server.set_version("2");
634        server.set_max_players(1);
635        let mut other = Net::new("other game");
636        other.join(&address);
637        let mut old = Net::new("game");
638        old.set_version("1");
639        old.join(&address);
640        let events = pump(&mut [&mut server, &mut other, &mut old], |n| !n[1].is_joining() && !n[2].is_joining());
641        assert_eq!(events[1], [NetEvent::Disconnected(PeerId::SERVER, Reason::WrongGame("game".into()))]);
642        assert_eq!(events[2], [NetEvent::Disconnected(PeerId::SERVER, Reason::WrongVersion("2".into()))]);
643
644        let mut first = Net::new("game");
645        first.set_version("2");
646        first.join(&address);
647        pump(&mut [&mut server, &mut first], |n| n[1].is_client());
648        let mut second = Net::new("game");
649        second.set_version("2");
650        second.join(&address);
651        let events = pump(&mut [&mut server, &mut second], |n| !n[1].is_joining());
652        assert_eq!(events[1], [NetEvent::Disconnected(PeerId::SERVER, Reason::ServerFull)]);
653    }
654
655    #[test]
656    fn kick_and_server_close() {
657        let (mut server, address) = host();
658        let mut a = Net::new("game");
659        let mut b = Net::new("game");
660        a.join(&address);
661        b.join(&address);
662        pump(&mut [&mut server, &mut a, &mut b], |n| n[0].player_count() == 2 && n[1].is_client() && n[2].is_client());
663
664        server.kick(a.id(), "spam");
665        let events = pump(&mut [&mut server, &mut a], |n| !n[1].is_online());
666        assert_eq!(events[1], [NetEvent::Disconnected(PeerId::SERVER, Reason::Kicked("spam".into()))]);
667
668        server.close();
669        let events = pump(&mut [&mut b], |n| !n[0].is_online());
670        assert_eq!(events[0], [NetEvent::Disconnected(PeerId::SERVER, Reason::Closed)]);
671    }
672
673    #[test]
674    fn join_failure_is_an_event() {
675        // Find a port nobody listens on.
676        let port = TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port();
677        let mut client = Net::new("game");
678        client.join(&format!("127.0.0.1:{port}"));
679        let events = pump(&mut [&mut client], |n| !n[0].is_joining());
680        assert!(matches!(events[0].as_slice(), [NetEvent::Disconnected(PeerId::SERVER, Reason::Failed(_))]));
681        // Sending while offline is ignored.
682        client.send_all("hello");
683    }
684}