Client/Server Feature Split (#63)

This commit is contained in:
PROMETHIA-27
2025-09-26 21:59:08 -04:00
committed by GitHub
parent 7f6c00b5d6
commit 2f5d154d26
30 changed files with 674 additions and 271 deletions

View File

@@ -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);
}
}