193 lines
5.9 KiB
Rust
193 lines
5.9 KiB
Rust
use bevy::{prelude::*, utils::synccell::SyncCell};
|
|
use lightyear::{
|
|
netcode::Key,
|
|
prelude::{client::NetcodeConfig, input::native::InputMarker, *},
|
|
};
|
|
use nil::prelude::Mutex;
|
|
use shared::{GameState, control::ControlState, global_observer, player::Player};
|
|
use std::{
|
|
env::current_exe,
|
|
io::{BufRead, BufReader},
|
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
|
process::Stdio,
|
|
sync::{LazyLock, mpsc},
|
|
};
|
|
|
|
/// Cache of server processes to be cleared at process exit
|
|
static SERVER_PROCESSES: LazyLock<Mutex<Vec<std::process::Child>>> = LazyLock::new(Mutex::default);
|
|
|
|
pub fn plugin(app: &mut App) {
|
|
app.add_systems(OnEnter(GameState::Connecting), attempt_connection);
|
|
app.add_systems(
|
|
Update,
|
|
parse_local_server_stdout.run_if(resource_exists::<LocalServerStdout>),
|
|
);
|
|
app.add_systems(Last, close_server_processes);
|
|
|
|
global_observer!(app, on_connecting);
|
|
global_observer!(app, on_connection_failed);
|
|
global_observer!(app, on_connection_succeeded);
|
|
global_observer!(app, temp_give_player_marker);
|
|
global_observer!(app, connect_on_local_server_started);
|
|
}
|
|
|
|
fn close_server_processes(mut app_exit: EventReader<AppExit>) {
|
|
if app_exit.read().next().is_some() {
|
|
let mut lock = SERVER_PROCESSES.lock();
|
|
for mut process in lock.drain(..) {
|
|
if let Err(err) = process.wait() {
|
|
error!("{err}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn attempt_connection(mut commands: Commands) -> Result {
|
|
let mut args = std::env::args();
|
|
let client_port = loop {
|
|
match args.next().as_deref() {
|
|
Some("--port") => {
|
|
break args.next().unwrap().parse::<u16>().unwrap();
|
|
}
|
|
Some(_) => (),
|
|
None => break 25564,
|
|
}
|
|
};
|
|
|
|
let client_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), client_port);
|
|
let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 25565);
|
|
let auth = Authentication::Manual {
|
|
server_addr,
|
|
client_id: client_port as u64,
|
|
private_key: Key::default(),
|
|
protocol_id: 0,
|
|
};
|
|
commands
|
|
.spawn((
|
|
Name::from("Client"),
|
|
Client::default(),
|
|
Link::new(None),
|
|
LocalAddr(client_addr),
|
|
PeerAddr(server_addr),
|
|
ReplicationReceiver::default(),
|
|
client::NetcodeClient::new(
|
|
auth,
|
|
NetcodeConfig {
|
|
client_timeout_secs: 1,
|
|
..default()
|
|
},
|
|
)?,
|
|
UdpIo::default(),
|
|
))
|
|
.trigger(Connect);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn on_connection_succeeded(
|
|
_trigger: Trigger<OnAdd, Connected>,
|
|
state: Res<State<GameState>>,
|
|
mut change_state: ResMut<NextState<GameState>>,
|
|
) {
|
|
if *state == GameState::Connecting {
|
|
change_state.set(GameState::Playing);
|
|
}
|
|
}
|
|
|
|
/// A client starts `Disconnected`, so in order to tell if it *actually* failed to connect/disconnected
|
|
/// vs. simply having been created, we need some extra state.
|
|
#[derive(Component)]
|
|
struct ClientActive;
|
|
|
|
fn on_connecting(trigger: Trigger<OnAdd, Connecting>, mut commands: Commands) {
|
|
commands.entity(trigger.target()).insert(ClientActive);
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct LocalServerStdout(SyncCell<mpsc::Receiver<String>>);
|
|
|
|
fn on_connection_failed(
|
|
trigger: Trigger<OnAdd, Disconnected>,
|
|
disconnected: Query<&Disconnected>,
|
|
mut commands: Commands,
|
|
client_active: Query<&ClientActive>,
|
|
mut opened_server: Local<bool>,
|
|
) {
|
|
let disconnected = disconnected.get(trigger.target()).unwrap();
|
|
if *opened_server {
|
|
panic!(
|
|
"failed to connect to local server: {:?}",
|
|
disconnected.reason
|
|
);
|
|
}
|
|
|
|
let client = trigger.target();
|
|
if client_active.contains(client) {
|
|
commands.entity(client).remove::<ClientActive>();
|
|
|
|
// the server executable is assumed to be adjacent to the client executable
|
|
let mut exe_path = current_exe().expect("failed to get path of client executable");
|
|
exe_path.set_file_name("server");
|
|
let mut server_process = std::process::Command::new(exe_path)
|
|
.args(["--timeout", "60", "--close-on-client-disconnect"])
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::null())
|
|
.spawn()
|
|
.expect("failed to start server");
|
|
let server_stdout = server_process.stdout.take().unwrap();
|
|
SERVER_PROCESSES.lock().push(server_process);
|
|
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
|
|
let stdout = BufReader::new(server_stdout).lines();
|
|
|
|
std::thread::spawn(move || {
|
|
for line in stdout {
|
|
match line {
|
|
Ok(line) => {
|
|
tx.send(line).unwrap();
|
|
}
|
|
Err(error) => {
|
|
error!("error reading local server stdout: `{error}`");
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
commands.insert_resource(LocalServerStdout(SyncCell::new(rx)));
|
|
|
|
*opened_server = true;
|
|
}
|
|
}
|
|
|
|
#[derive(Event)]
|
|
struct LocalServerStarted;
|
|
|
|
fn parse_local_server_stdout(mut commands: Commands, mut stdout: ResMut<LocalServerStdout>) {
|
|
let stdout: &mut LocalServerStdout = &mut stdout;
|
|
|
|
while let Ok(line) = stdout.0.get().try_recv() {
|
|
if let "hedz.server_started" = &line[..] {
|
|
commands.trigger(LocalServerStarted);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn connect_on_local_server_started(
|
|
_trigger: Trigger<LocalServerStarted>,
|
|
state: Res<State<GameState>>,
|
|
mut commands: Commands,
|
|
client: Single<Entity, With<Client>>,
|
|
) {
|
|
if *state == GameState::Connecting {
|
|
commands.entity(*client).trigger(Connect);
|
|
}
|
|
}
|
|
|
|
fn temp_give_player_marker(trigger: Trigger<OnAdd, Player>, mut commands: Commands) {
|
|
commands
|
|
.entity(trigger.target())
|
|
.insert(InputMarker::<ControlState>::default());
|
|
}
|