use alloc::{format, string::ToString};
use crate::Error;
use crate::auth::Authentication;
use crate::client::{ClientConfig, ClientState};
use aeronet_io::connection::PeerAddr;
use bevy_app::{App, Plugin, PostUpdate, PreUpdate};
use bevy_ecs::lifecycle::HookContext;
use bevy_ecs::prelude::*;
use bevy_ecs::{system::ParallelCommands, world::DeferredWorld};
use bevy_reflect::Reflect;
use bevy_time::{Real, Time};
use lightyear_connection::ConnectionSystems;
use lightyear_connection::client::{
Connect, Connected, Connecting, ConnectionPlugin, Disconnect, Disconnected,
};
use lightyear_connection::host::HostClient;
use lightyear_core::id::{LocalId, PeerId, RemoteId};
use lightyear_link::{Link, LinkSystems, Linked};
use lightyear_transport::plugin::TransportSystems;
use tracing::{debug, error, info};
pub struct NetcodeClientPlugin;
#[derive(Component)]
#[require(Link, lightyear_connection::client::Client)]
#[require(Disconnected)]
#[component(on_insert = NetcodeClient::on_insert)]
pub struct NetcodeClient {
pub inner: crate::client::Client<()>,
}
impl NetcodeClient {
fn on_insert(mut world: DeferredWorld, context: HookContext) {
if let Some(server_addr) = world
.get::<NetcodeClient>(context.entity)
.map(|client| client.inner.server_addr())
{
world
.commands()
.get_entity(context.entity)
.unwrap()
.insert(PeerAddr(server_addr));
}
}
}
#[derive(Clone, Reflect)]
pub struct NetcodeConfig {
pub num_disconnect_packets: usize,
pub keepalive_packet_send_rate: f64,
pub client_timeout_secs: i32,
pub token_expire_secs: i32,
}
impl Default for NetcodeConfig {
fn default() -> Self {
Self {
num_disconnect_packets: 10,
keepalive_packet_send_rate: 1.0 / 10.0,
client_timeout_secs: 3,
token_expire_secs: 30,
}
}
}
impl NetcodeConfig {
pub(crate) fn build(&self) -> ClientConfig<()> {
ClientConfig::default()
.num_disconnect_packets(self.num_disconnect_packets)
.packet_send_rate(self.keepalive_packet_send_rate)
}
}
impl NetcodeClient {
pub fn new(auth: Authentication, config: NetcodeConfig) -> Result<Self, Error> {
let token = auth.get_token(config.client_timeout_secs, config.token_expire_secs)?;
let token_bytes = token.try_into_bytes()?;
Ok(Self {
inner: crate::client::Client::with_config(&token_bytes, config.build())?,
})
}
pub fn id(&self) -> PeerId {
PeerId::Netcode(self.inner.id())
}
}
impl NetcodeClientPlugin {
fn send(
mut query: Query<(&mut Link, &mut NetcodeClient), (With<Linked>, Without<HostClient>)>,
) {
query.par_iter_mut().for_each(|(mut link, mut client)| {
for _ in 0..link.send.len() {
if let Some(payload) = link.send.pop() {
client
.inner
.send(payload, &mut link.send)
.inspect_err(|e| {
error!("Error sending packet: {:?}", e);
})
.ok();
}
}
client.inner.drain_send_netcode_packets(&mut link.send);
})
}
fn receive(
real_time: Res<Time<Real>>,
mut query: Query<
(
Entity,
&mut Link,
&mut NetcodeClient,
Has<Connecting>,
Has<Disconnected>,
),
(With<Linked>, Without<HostClient>),
>,
parallel_commands: ParallelCommands,
) {
let delta = real_time.delta();
query
.par_iter_mut()
.for_each(|(entity, mut link, mut client, connecting, disconnected)| {
if let Ok(state) = client
.inner
.try_update(delta.as_secs_f64(), &mut link.recv)
.inspect_err(|e| {
error!("Error receiving packet: {:?}", e);
})
{
if state == ClientState::Connected && connecting {
info!("Client {} connected", client.id());
parallel_commands.command_scope(|mut commands| {
commands.entity(entity).insert((
Connected,
LocalId(client.id()),
RemoteId(PeerId::Server),
));
});
}
if !disconnected
&& !matches!(
state,
ClientState::Connected
| ClientState::SendingConnectionRequest
| ClientState::SendingChallengeResponse
)
{
info!("Client {} disconnected. State: {state:?}", client.id());
parallel_commands.command_scope(|mut commands| {
commands.entity(entity).insert(Disconnected {
reason: Some(format!("Client disconnected: {state:?}")),
});
});
}
}
})
}
fn connect(
trigger: On<Connect>,
mut commands: Commands,
mut query: Query<&mut NetcodeClient, Without<Connected>>,
) {
if let Ok(mut client) = query.get_mut(trigger.entity) {
debug!("Starting netcode connection process");
client.inner.connect();
commands.entity(trigger.entity).insert(Connecting);
}
}
fn disconnect(
trigger: On<Disconnect>,
mut commands: Commands,
mut query: Query<&mut NetcodeClient, Without<Disconnected>>,
) -> Result {
if let Ok(mut client) = query.get_mut(trigger.entity) {
client.inner.disconnect()?;
commands.entity(trigger.entity).insert(Disconnected {
reason: Some("Client trigger".to_string()),
});
}
Ok(())
}
}
impl Plugin for NetcodeClientPlugin {
fn build(&self, app: &mut App) {
if !app.is_plugin_added::<ConnectionPlugin>() {
app.add_plugins(ConnectionPlugin);
}
app.configure_sets(
PreUpdate,
(
LinkSystems::Receive,
ConnectionSystems::Receive,
TransportSystems::Receive,
)
.chain(),
);
app.configure_sets(
PostUpdate,
(
TransportSystems::Send,
ConnectionSystems::Send,
LinkSystems::Send,
)
.chain(),
);
app.add_systems(PreUpdate, Self::receive.in_set(ConnectionSystems::Receive));
app.add_systems(PostUpdate, Self::send.in_set(ConnectionSystems::Send));
app.add_observer(Self::connect);
app.add_observer(Self::disconnect);
}
}