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>> = 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::), ); 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) { 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::().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, state: Res>, mut change_state: ResMut>, ) { 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, mut commands: Commands) { commands.entity(trigger.target()).insert(ClientActive); } #[derive(Resource)] struct LocalServerStdout(SyncCell>); fn on_connection_failed( trigger: Trigger, disconnected: Query<&Disconnected>, mut commands: Commands, client_active: Query<&ClientActive>, mut opened_server: Local, ) { 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::(); // 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) { 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, state: Res>, mut commands: Commands, client: Single>, ) { if *state == GameState::Connecting { commands.entity(*client).trigger(Connect); } } fn temp_give_player_marker(trigger: Trigger, mut commands: Commands) { commands .entity(trigger.target()) .insert(InputMarker::::default()); }