Client/Server Feature Split (#63)
This commit is contained in:
@@ -1,26 +1,48 @@
|
||||
use bevy::prelude::*;
|
||||
use bevy::{prelude::*, utils::synccell::SyncCell};
|
||||
use lightyear::{
|
||||
connection::client::ClientState,
|
||||
netcode::Key,
|
||||
prelude::{client::NetcodeConfig, input::native::InputMarker, *},
|
||||
};
|
||||
use shared::{
|
||||
GameState, control::ControlState, global_observer, heads_database::HeadsDatabase,
|
||||
player::Player, tb_entities::SpawnPoint,
|
||||
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},
|
||||
};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
|
||||
/// 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(Startup, temp_connect_on_startup);
|
||||
app.add_systems(OnEnter(GameState::Connecting), attempt_connection);
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
spawn_disconnected_player.run_if(in_state(GameState::Playing)),
|
||||
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 temp_connect_on_startup(mut commands: Commands) -> Result {
|
||||
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() {
|
||||
@@ -44,9 +66,9 @@ fn temp_connect_on_startup(mut commands: Commands) -> Result {
|
||||
.spawn((
|
||||
Name::from("Client"),
|
||||
Client::default(),
|
||||
Link::new(None),
|
||||
LocalAddr(client_addr),
|
||||
PeerAddr(server_addr),
|
||||
Link::new(None),
|
||||
ReplicationReceiver::default(),
|
||||
client::NetcodeClient::new(
|
||||
auth,
|
||||
@@ -62,15 +84,104 @@ fn temp_connect_on_startup(mut commands: Commands) -> Result {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn_disconnected_player(
|
||||
disconnected: Single<&Client, Changed<Client>>,
|
||||
commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
query: Query<&Transform, With<SpawnPoint>>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
fn on_connection_succeeded(
|
||||
_trigger: Trigger<OnAdd, Connected>,
|
||||
state: Res<State<GameState>>,
|
||||
mut change_state: ResMut<NextState<GameState>>,
|
||||
) {
|
||||
if disconnected.state == ClientState::Disconnected {
|
||||
shared::player::spawn(commands, Entity::PLACEHOLDER, query, asset_server, heads_db)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user