use avian3d::prelude::{ Collider, ColliderAabb, ColliderDensity, ColliderMarker, ColliderMassProperties, CollisionEventsEnabled, CollisionLayers, Sensor, }; use bevy::{ ecs::bundle::BundleFromComponents, prelude::*, scene::SceneInstance, utils::synccell::SyncCell, }; use bevy_trenchbroom::geometry::Brushes; use lightyear::{ link::{LinkConditioner, prelude::*}, netcode::Key, prelude::{ client::{Input, InputDelayConfig, NetcodeConfig}, input::native::InputMarker, *, }, }; use nil::prelude::Mutex; use shared::{ GameState, control::ControlState, global_observer, player::Player, protocol::{ ClientEnteredPlaying, TbMapEntityId, TbMapEntityMapping, channels::UnorderedReliableChannel, messages::DespawnTbMapEntity, }, tb_entities::{Platform, PlatformTarget}, }; use std::{ env::current_exe, fs::File, io::{BufRead, BufReader}, net::{IpAddr, Ipv4Addr, SocketAddr}, process::Stdio, sync::{LazyLock, mpsc}, time::Duration, }; /// 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); app.add_systems(Update, despawn_absent_map_entities); 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); global_observer!(app, received_remote_map_entity); } 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, }; let sync_config = SyncConfig { jitter_multiple: 5, jitter_margin: Duration::from_millis(15), ..default() }; let conditioner = LinkConditioner::new(LinkConditionerConfig { incoming_latency: Duration::from_millis(10), incoming_jitter: Duration::from_millis(0), incoming_loss: 0.0, }); commands .spawn(( Name::from("Client"), Client::default(), Link::new(Some(conditioner)), LocalAddr(client_addr), PeerAddr(server_addr), ReplicationReceiver::default(), client::NetcodeClient::new( auth, NetcodeConfig { client_timeout_secs: 1, ..default() }, )?, UdpIo::default(), InputTimeline(Timeline::from(Input::new( sync_config, InputDelayConfig::balanced(), ))), )) .trigger(Connect); Ok(()) } fn on_connection_succeeded( _trigger: Trigger, state: Res>, mut change_state: ResMut>, mut sender: Single<&mut TriggerSender>, ) { if *state == GameState::Connecting { change_state.set(GameState::Playing); sender.trigger::(ClientEnteredPlaying); } } /// 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, ) -> Result { 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 server_log_file = File::create("server.log")?; let mut server_process = std::process::Command::new(exe_path) .args(["--timeout", "60", "--close-on-client-disconnect"]) .env("NO_COLOR", "1") .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(server_log_file) .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; } Ok(()) } #[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); } else { info!("SERVER: {line}"); } } } 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()); } fn received_remote_map_entity( trigger: Trigger, world: &mut World, mut child_buffer: Local>, ) { let serverside = trigger.target(); if world.get::(serverside).is_none() { return; } let id = *world.get::(serverside).unwrap(); let Some(clientside) = world.resource_mut::().0.remove(&id.id) else { warn!("received unknown MapEntity ID `{id:?}`"); return; }; // cannot just use `take` directly with a bundle because then any missing component would cause // the entire bundle to fail move_component::(world, clientside, serverside); move_component::<( Collider, ColliderAabb, ColliderDensity, ColliderMarker, ColliderMassProperties, CollisionLayers, )>(world, clientside, serverside); move_component::(world, clientside, serverside); move_component::(world, clientside, serverside); move_component::(world, clientside, serverside); move_component::(world, clientside, serverside); move_component::(world, clientside, serverside); move_component::(world, clientside, serverside); if let Some(children) = world.get::(clientside) { child_buffer.extend(children.iter()); for child in child_buffer.drain(..) { world.entity_mut(child).insert(ChildOf(serverside)); } } world.entity_mut(clientside).despawn(); } fn move_component(world: &mut World, from: Entity, to: Entity) { let comp = world.entity_mut(from).take::(); if let Some(comp) = comp { world.entity_mut(to).insert(comp); } } fn despawn_absent_map_entities( mut commands: Commands, mut messages: Query<&mut MessageReceiver>, mut map: ResMut, ) { for mut recv in messages.iter_mut() { for msg in recv.receive() { // the server may double-send DespawnTbMapEntity for a given ID, so ignore it if the entity // was already despawned. let Some(entity) = map.0.remove(&msg.0) else { continue; }; commands.entity(entity).despawn(); } } }