- 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
1715f51b35
13 changed files with 435 additions and 115 deletions
@ -0,0 +1,67 @@ |
|||||||
|
use clap::{Parser, Subcommand}; |
||||||
|
use common::network::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.
|
||||||
|
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 bevy::prelude::*; |
||||||
use lightyear::prelude::client::*; |
use lightyear::prelude::client::*; |
||||||
use lightyear::prelude::*; |
use lightyear::prelude::*; |
||||||
|
|
||||||
// FIXME: Don't hardcode this!
|
pub use super::client_webtransport::ConnectWebTransportCommand; |
||||||
pub const DIGEST: &'static str = ""; |
|
||||||
|
|
||||||
pub struct ClientPlugin; |
pub struct ClientPlugin; |
||||||
|
|
||||||
impl Plugin for ClientPlugin { |
impl Plugin for ClientPlugin { |
||||||
fn build(&self, app: &mut App) { |
fn build(&self, app: &mut App) { |
||||||
app.add_plugins(ClientPlugins::default()); |
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>() { |
if !app.is_plugin_added::<super::ProtocolPlugin>() { |
||||||
app.add_plugins(super::ProtocolPlugin); |
app.add_plugins(super::ProtocolPlugin); |
||||||
} |
} |
||||||
|
|
||||||
app.add_systems(Startup, connect_to_server); |
|
||||||
|
|
||||||
app.add_observer(on_connecting); |
app.add_observer(on_connecting); |
||||||
app.add_observer(on_connected); |
app.add_observer(on_connected); |
||||||
app.add_observer(on_disconnected); |
app.add_observer(on_disconnected); |
||||||
|
|
||||||
|
app.add_observer(autoconnect_host_client); |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
fn connect_to_server(mut commands: Commands) { |
/// Automatically creates a "host client" to connect to the local server when it is started.
|
||||||
let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0); |
fn autoconnect_host_client(event: On<Add, server::Started>, mut commands: Commands) { |
||||||
let server_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), super::DEFAULT_PORT); |
let server = event.entity; |
||||||
let certificate_digest = DIGEST.to_string(); |
|
||||||
|
|
||||||
commands |
commands |
||||||
.spawn(( |
.spawn(( |
||||||
Name::from("Client"), |
Client::default(), |
||||||
LocalAddr(client_addr), |
Name::from("HostClient"), |
||||||
PeerAddr(server_addr), |
|
||||||
ReplicationReceiver::default(), |
ReplicationReceiver::default(), |
||||||
WebTransportClientIo { certificate_digest }, |
LinkOf { server }, |
||||||
RawClient, |
|
||||||
)) |
)) |
||||||
.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; |
let client = event.entity; |
||||||
|
if !clients.contains(client) { |
||||||
|
return; // Not a client we started. (server-side?)
|
||||||
|
}; |
||||||
info!("Client '{client}' connecting ..."); |
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; |
let client = event.entity; |
||||||
|
if !clients.contains(client) { |
||||||
|
return; // Not a client we started. (server-side?)
|
||||||
|
}; |
||||||
info!("Client '{client}' connected!"); |
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; |
let client = event.entity; |
||||||
|
if !clients.contains(client) { |
||||||
|
return; // Not a client we started. (server-side?)
|
||||||
|
}; |
||||||
info!("Client '{client}' disconnected!"); |
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