Files
HEDZReloaded/crates/server/src/server.rs
2025-10-07 16:31:17 -03:00

158 lines
4.3 KiB
Rust

use crate::config::ServerConfig;
use bevy::prelude::*;
use lightyear::{
connection::client::PeerMetadata,
link::LinkConditioner,
prelude::{
server::{ClientOf, NetcodeConfig, NetcodeServer, ServerUdpIo, Started},
*,
},
};
use shared::{
GameState, global_observer,
heads_database::HeadsDatabase,
protocol::{
ClientEnteredPlaying, channels::UnorderedReliableChannel, messages::AssignClientPlayer,
},
tb_entities::SpawnPoint,
};
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
time::Duration,
};
pub fn plugin(app: &mut App) {
app.add_systems(
OnEnter(GameState::Playing),
(start_server, setup_timeout_timer),
);
app.add_systems(
Update,
(notify_started, run_timeout).run_if(in_state(GameState::Playing)),
);
global_observer!(app, handle_new_client);
global_observer!(app, on_client_connected);
global_observer!(app, on_client_playing);
global_observer!(app, close_on_disconnect);
global_observer!(app, cancel_timeout);
}
#[derive(Component)]
struct ClientPlayerId(u8);
fn handle_new_client(
trigger: Trigger<OnAdd, Linked>,
mut commands: Commands,
id: Query<&PeerAddr>,
) -> Result {
let Ok(id) = id.get(trigger.target()) else {
return Ok(());
};
info!("Client connected on IP: {}", id.ip());
let conditioner = LinkConditioner::new(LinkConditionerConfig {
incoming_latency: Duration::from_millis(10),
incoming_jitter: Duration::from_millis(0),
incoming_loss: 0.0,
});
commands.entity(trigger.target()).insert((
ReplicationSender::default(),
Link::new(Some(conditioner)),
ClientPlayerId(0),
));
Ok(())
}
fn on_client_connected(
trigger: Trigger<OnAdd, ClientOf>,
mut assign_player: Query<(&ClientPlayerId, &mut MessageSender<AssignClientPlayer>)>,
) -> Result {
// `Linked` happens before the `ClientOf` and `MessageSender` components are added, so the server can't
// send the client player id until now.
let (id, mut sender) = assign_player.get_mut(trigger.target())?;
sender.send::<UnorderedReliableChannel>(AssignClientPlayer(id.0));
Ok(())
}
fn on_client_playing(
trigger: Trigger<RemoteTrigger<ClientEnteredPlaying>>,
commands: Commands,
query: Query<&Transform, With<SpawnPoint>>,
heads_db: Res<HeadsDatabase>,
peers: Res<PeerMetadata>,
) -> Result {
let Some(&client) = peers.mapping.get(&trigger.from) else {
return Ok(());
};
crate::player::spawn(commands, client, query, heads_db).ok_or("failed to spawn player")?;
Ok(())
}
fn close_on_disconnect(
_trigger: Trigger<OnRemove, Connected>,
config: Res<ServerConfig>,
mut writer: EventWriter<AppExit>,
) {
if config.close_on_client_disconnect {
info!("client disconnected, exiting");
writer.write(AppExit::Success);
}
}
fn start_server(mut commands: Commands) -> Result {
let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 25565);
let conditioner = LinkConditioner::new(LinkConditionerConfig {
incoming_latency: Duration::from_millis(10),
incoming_jitter: Duration::from_millis(0),
incoming_loss: 0.0,
});
let mut commands = commands.spawn((
Name::from("Server"),
LocalAddr(server_addr),
ServerUdpIo::default(),
NetcodeServer::new(NetcodeConfig {
client_timeout_secs: 3,
..Default::default()
}),
Link::new(Some(conditioner)),
));
commands.trigger(server::Start);
Ok(())
}
fn notify_started(started: Query<&Started>, mut notified: Local<bool>) {
if !*notified && !started.is_empty() {
println!("hedz.server_started");
*notified = true;
}
}
#[derive(Resource)]
struct TimeoutTimer(f32);
fn setup_timeout_timer(mut commands: Commands, config: Res<ServerConfig>) {
commands.insert_resource(TimeoutTimer(config.timeout));
}
fn run_timeout(mut timer: ResMut<TimeoutTimer>, mut writer: EventWriter<AppExit>, time: Res<Time>) {
timer.0 -= time.delta_secs();
if timer.0 <= 0.0 {
info!("client timed out, exiting");
writer.write(AppExit::Success);
}
}
fn cancel_timeout(_trigger: Trigger<OnAdd, Connected>, mut timer: ResMut<TimeoutTimer>) {
info!("client connected, cancelling timeout");
timer.0 = f32::INFINITY;
}