- Parse CLI arguments using `clap` - Or get them from browser location on web - Support `Local`, `Host` and `Connect` modes - Use commands for starting and connecting - Only `spawn_initial_blocks` when server is started - Use `autoconnect_host_client` for local servers - Correctly handle client and server eventsmain
parent
bb66d28f06
commit
54615c265d
13 changed files with 437 additions and 115 deletions
@ -0,0 +1,68 @@ |
||||
use clap::{Parser, Subcommand}; |
||||
use common::network::{DEFAULT_ADDRESS, DEFAULT_PORT}; |
||||
|
||||
#[derive(Parser, Default, Debug)] |
||||
#[command(version, about)] |
||||
pub struct Args { |
||||
#[command(subcommand)] |
||||
pub mode: Option<Mode>, |
||||
} |
||||
|
||||
#[derive(Subcommand, Debug)] |
||||
pub enum Mode { |
||||
/// Play the game on your own.
|
||||
Local, |
||||
/// Host a server and play on it.
|
||||
#[cfg(not(target_family = "wasm"))] |
||||
Host { |
||||
#[arg(default_value_t = DEFAULT_PORT)] |
||||
port: u16, |
||||
}, |
||||
/// Connect to an existing server.
|
||||
Connect { |
||||
/// Address to connect to.
|
||||
#[arg(default_value = DEFAULT_ADDRESS)] |
||||
address: String, |
||||
/// Certificate digest provided by the server.
|
||||
digest: String, |
||||
}, |
||||
} |
||||
|
||||
impl Default for Mode { |
||||
fn default() -> Self { |
||||
Self::Local |
||||
} |
||||
} |
||||
|
||||
impl Args { |
||||
#[cfg(not(target_family = "wasm"))] |
||||
pub fn parse() -> Self { |
||||
<Args as Parser>::parse() |
||||
} |
||||
|
||||
#[cfg(target_family = "wasm")] |
||||
pub fn parse() -> Self { |
||||
use bevy::log::error; |
||||
use web_sys::{UrlSearchParams, window}; |
||||
|
||||
let Some(window) = window() else { |
||||
return Self::default(); |
||||
}; |
||||
let Ok(search) = window.location().search() else { |
||||
return Self::default(); |
||||
}; |
||||
let Ok(params) = UrlSearchParams::new_with_str(&search) else { |
||||
return Self::default(); |
||||
}; |
||||
let Some(address) = params.get("connect") else { |
||||
return Self::default(); |
||||
}; |
||||
let Some(digest) = params.get("digest") else { |
||||
error!("Missing 'digest' parameter."); |
||||
return Self::default(); |
||||
}; |
||||
Self { |
||||
mode: Some(Mode::Connect { address, digest }), |
||||
} |
||||
} |
||||
} |
||||
@ -1,63 +1,60 @@ |
||||
use std::net::{Ipv4Addr, SocketAddr}; |
||||
|
||||
use bevy::prelude::*; |
||||
use lightyear::prelude::client::*; |
||||
use lightyear::prelude::*; |
||||
|
||||
// FIXME: Don't hardcode this!
|
||||
pub const DIGEST: &'static str = ""; |
||||
pub use super::client_webtransport::ConnectWebTransportCommand; |
||||
|
||||
pub struct ClientPlugin; |
||||
|
||||
impl Plugin for ClientPlugin { |
||||
fn build(&self, app: &mut App) { |
||||
app.add_plugins(ClientPlugins::default()); |
||||
// This maybe should be added by `ClientPlugins` but it currently isn't.
|
||||
// (Unless we're using `NetcodeClientPlugin`, which would've added it.)
|
||||
if !app.is_plugin_added::<lightyear::connection::client::ConnectionPlugin>() { |
||||
app.add_plugins(lightyear::connection::client::ConnectionPlugin); |
||||
} |
||||
|
||||
if !app.is_plugin_added::<super::ProtocolPlugin>() { |
||||
app.add_plugins(super::ProtocolPlugin); |
||||
} |
||||
|
||||
app.add_systems(Startup, connect_to_server); |
||||
|
||||
app.add_observer(on_connecting); |
||||
app.add_observer(on_connected); |
||||
app.add_observer(on_disconnected); |
||||
|
||||
app.add_observer(autoconnect_host_client); |
||||
} |
||||
} |
||||
|
||||
fn connect_to_server(mut commands: Commands) { |
||||
let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0); |
||||
let server_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), super::DEFAULT_PORT); |
||||
let certificate_digest = DIGEST.to_string(); |
||||
|
||||
/// Automatically creates a "host client" to connect to the local server when it is started.
|
||||
fn autoconnect_host_client(event: On<Add, server::Started>, mut commands: Commands) { |
||||
let server = event.entity; |
||||
commands |
||||
.spawn(( |
||||
Name::from("Client"), |
||||
LocalAddr(client_addr), |
||||
PeerAddr(server_addr), |
||||
Client::default(), |
||||
Name::from("HostClient"), |
||||
ReplicationReceiver::default(), |
||||
WebTransportClientIo { certificate_digest }, |
||||
RawClient, |
||||
LinkOf { server }, |
||||
)) |
||||
.trigger(|entity| LinkStart { entity }); |
||||
.trigger(|entity| Connect { entity }); |
||||
} |
||||
|
||||
fn on_connecting(event: On<Add, Connecting>) { |
||||
fn on_connecting(event: On<Add, Connecting>, clients: Query<(), With<Client>>) { |
||||
let client = event.entity; |
||||
if !clients.contains(client) { |
||||
return; // Not a client we started. (server-side?)
|
||||
}; |
||||
info!("Client '{client}' connecting ..."); |
||||
} |
||||
|
||||
fn on_connected(event: On<Add, Connected>) { |
||||
fn on_connected(event: On<Add, Connected>, clients: Query<(), With<Client>>) { |
||||
let client = event.entity; |
||||
if !clients.contains(client) { |
||||
return; // Not a client we started. (server-side?)
|
||||
}; |
||||
info!("Client '{client}' connected!"); |
||||
} |
||||
|
||||
fn on_disconnected(event: On<Remove, Connected>) { |
||||
fn on_disconnected(event: On<Remove, Connected>, clients: Query<(), With<Client>>) { |
||||
let client = event.entity; |
||||
if !clients.contains(client) { |
||||
return; // Not a client we started. (server-side?)
|
||||
}; |
||||
info!("Client '{client}' disconnected!"); |
||||
} |
||||
|
||||
@ -0,0 +1,63 @@ |
||||
use std::net::{Ipv4Addr, SocketAddr, ToSocketAddrs}; |
||||
|
||||
use bevy::prelude::*; |
||||
use lightyear::prelude::client::*; |
||||
use lightyear::prelude::*; |
||||
|
||||
use thiserror::Error; |
||||
|
||||
pub struct ConnectWebTransportCommand { |
||||
server_addr: SocketAddr, |
||||
certificate_digest: String, |
||||
} |
||||
|
||||
impl ConnectWebTransportCommand { |
||||
pub fn new(server_addr: &str, certificate_digest: String) -> Result<Self> { |
||||
let server_addr = to_socket_addr_with_default_port(server_addr, super::DEFAULT_PORT)?; |
||||
Ok(Self { |
||||
server_addr, |
||||
certificate_digest, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
impl Command for ConnectWebTransportCommand { |
||||
fn apply(self, world: &mut World) -> () { |
||||
let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0); |
||||
let certificate_digest = self.certificate_digest; |
||||
world |
||||
.spawn(( |
||||
Client::default(), |
||||
Name::from("Client"), |
||||
LocalAddr(client_addr), |
||||
PeerAddr(self.server_addr), |
||||
ReplicationReceiver::default(), |
||||
WebTransportClientIo { certificate_digest }, |
||||
RawClient, |
||||
)) |
||||
.trigger(|entity| Connect { entity }); |
||||
} |
||||
} |
||||
|
||||
// TODO: Move this to its own `utils` module?
|
||||
// FIXME: Hostname resolving does not work on web.
|
||||
fn to_socket_addr_with_default_port(addr: &str, default_port: u16) -> Result<SocketAddr> { |
||||
let has_port = match (addr.rfind(']'), addr.rfind(':')) { |
||||
// This doesn't match colons within IPv6 brackets, like `[::1]`.
|
||||
(Some(bracket), Some(colon)) if bracket < colon => true, |
||||
(None, Some(_)) => true, |
||||
_ => false, |
||||
}; |
||||
|
||||
let mut socket_addrs = if has_port { |
||||
addr.to_socket_addrs() |
||||
} else { |
||||
(addr, default_port).to_socket_addrs() |
||||
}?; |
||||
|
||||
socket_addrs.next().ok_or(ResolveError.into()) |
||||
} |
||||
|
||||
#[derive(Error, Debug)] |
||||
#[error("hostname could not be resolved to any address")] |
||||
pub struct ResolveError; |
||||
@ -0,0 +1,60 @@ |
||||
#![cfg(not(target_family = "wasm"))] |
||||
|
||||
use std::net::{Ipv4Addr, SocketAddr}; |
||||
|
||||
use bevy::prelude::*; |
||||
use lightyear::prelude::server::*; |
||||
use lightyear::prelude::*; |
||||
|
||||
pub struct StartWebTransportServerCommand { |
||||
server_addr: SocketAddr, |
||||
certificate: Identity, |
||||
} |
||||
|
||||
impl StartWebTransportServerCommand { |
||||
pub fn new(port: u16) -> StartWebTransportServerCommand { |
||||
Self { |
||||
// TODO: Allow specifying this?
|
||||
server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port), |
||||
certificate: Identity::self_signed(["localhost", "127.0.0.1", "::1"]).unwrap(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl Default for StartWebTransportServerCommand { |
||||
fn default() -> Self { |
||||
Self::new(super::DEFAULT_PORT) |
||||
} |
||||
} |
||||
|
||||
impl Command for StartWebTransportServerCommand { |
||||
fn apply(self, world: &mut World) { |
||||
world |
||||
.spawn(( |
||||
Server::default(), |
||||
Name::from("Server"), |
||||
LocalAddr(self.server_addr), |
||||
WebTransportServerIo { |
||||
certificate: self.certificate, |
||||
}, |
||||
RawServer, |
||||
)) |
||||
.trigger(|entity| Start { entity }); |
||||
} |
||||
} |
||||
|
||||
pub(crate) fn print_certificate_digest( |
||||
event: On<Add, Started>, |
||||
servers: Query<&WebTransportServerIo>, |
||||
) -> Result { |
||||
let server = event.entity; |
||||
let certificate = &servers.get(server)?.certificate; |
||||
let certificate_hash = certificate.certificate_chain().as_slice()[0].hash(); |
||||
let certificate_digest = certificate_hash.to_string().replace(':', ""); |
||||
|
||||
info!("== Certificate Digest =="); |
||||
info!(" {certificate_digest}"); |
||||
info!(" (Clients use this to securely connect to the server.)"); |
||||
|
||||
Ok(()) |
||||
} |
||||
Loading…
Reference in new issue