#![no_std]
extern crate alloc;
use aeronet_io::connection::{LocalAddr, PeerAddr};
use alloc::string::String;
use bevy_app::{App, Plugin, PostUpdate, PreUpdate};
use bevy_ecs::prelude::*;
use bevy_ecs::query::QueryData;
use bytes::Bytes;
use core::net::{Ipv4Addr, SocketAddr};
use crossbeam_channel::{Receiver, Sender, TryRecvError, TrySendError};
use lightyear_core::time::Instant;
use lightyear_link::{
Link, LinkPlugin, LinkReceiveSystems, LinkStart, LinkSystems, Linked, Unlink, UnlinkReason,
recv_payload_from_bytes,
};
use tracing::{error, trace};
pub(crate) const MTU: usize = 1472;
const LOCALHOST: SocketAddr = SocketAddr::new(core::net::IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
#[derive(Component, Clone)]
#[require(Link::default())]
#[require(LocalAddr(LOCALHOST))]
#[require(PeerAddr(LOCALHOST))]
pub struct CrossbeamIo {
sender: Sender<Bytes>,
receiver: Receiver<Bytes>,
}
impl CrossbeamIo {
pub fn new(sender: Sender<Bytes>, receiver: Receiver<Bytes>) -> Self {
Self { sender, receiver }
}
pub fn new_pair() -> (Self, Self) {
let (sender1, receiver1) = crossbeam_channel::unbounded();
let (sender2, receiver2) = crossbeam_channel::unbounded();
(Self::new(sender1, receiver2), Self::new(sender2, receiver1))
}
}
pub struct CrossbeamPlugin;
#[derive(QueryData)]
#[query_data(mutable)]
struct IOQuery {
entity: Entity,
link: &'static mut Link,
crossbeam_io: &'static CrossbeamIo,
#[cfg(feature = "test_utils")]
helper: Option<&'static lightyear_core::test::TestHelper>,
}
impl CrossbeamPlugin {
fn link(
link_start: On<LinkStart>,
query: Query<(), With<CrossbeamIo>>,
mut commands: Commands,
) {
if query.get(link_start.entity).is_ok() {
trace!(
"Immediately add Linked for CrossbeamIO entity: {:?}",
link_start.entity
);
commands.entity(link_start.entity).insert(Linked);
}
}
fn send(mut query: Query<IOQuery, With<Linked>>, mut commands: Commands) {
for mut io in query.iter_mut() {
let entity = io.entity;
while let Some(payload) = io.link.send.pop() {
#[cfg(feature = "test_utils")]
if io.helper.is_some_and(|h| h.block_send) {
continue;
}
match io.crossbeam_io.sender.try_send(payload) {
Ok(()) => {}
Err(TrySendError::Disconnected(_)) => {
trace!(
"CrossbeamIo send dropped on entity {entity:?}: channel disconnected"
);
let _ = io.link.send.drain();
commands.trigger(Unlink {
entity,
reason: UnlinkReason::TransportError(String::from(
"Crossbeam channel disconnected",
)),
});
break;
}
Err(TrySendError::Full(p)) => {
error!(
"CrossbeamIo send: channel full on entity {entity:?} (transport assumes unbounded); re-queueing"
);
io.link.send.push_front(p);
break;
}
}
}
}
}
fn receive(
mut query: Query<(Entity, &mut Link, &CrossbeamIo), With<Linked>>,
mut commands: Commands,
) {
for (entity, mut link, crossbeam_io) in query.iter_mut() {
loop {
match crossbeam_io.receiver.try_recv() {
Ok(data) => {
trace!("recv data: {data:?}");
link.recv
.push(recv_payload_from_bytes(data), Instant::now())
}
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => {
trace!(
"CrossbeamIo receive dropped on entity {entity:?}: channel disconnected"
);
commands.trigger(Unlink {
entity,
reason: UnlinkReason::TransportError(String::from(
"Crossbeam channel disconnected",
)),
});
break;
}
}
}
}
}
}
impl Plugin for CrossbeamPlugin {
fn build(&self, app: &mut App) {
if !app.is_plugin_added::<LinkPlugin>() {
app.add_plugins(LinkPlugin);
}
app.add_observer(Self::link);
app.add_systems(
PreUpdate,
Self::receive.in_set(LinkReceiveSystems::BufferToLink),
);
app.add_systems(PostUpdate, Self::send.in_set(LinkSystems::Send));
}
}
#[cfg(test)]
mod tests {
use super::*;
use lightyear_connection::prelude::{Connected, Disconnected};
use lightyear_link::LinkState;
#[test]
fn send_after_peer_disconnect_unlinks_transport() {
let (sender, peer_receiver) = crossbeam_channel::unbounded::<Bytes>();
let (peer_sender, receiver) = crossbeam_channel::unbounded::<Bytes>();
let client_io = CrossbeamIo::new(sender, receiver);
let mut app = App::new();
app.add_plugins(CrossbeamPlugin);
let sender_entity = app
.world_mut()
.spawn((Link::default(), Linked, client_io))
.id();
drop(peer_receiver);
let mut link = app
.world_mut()
.get_mut::<Link>(sender_entity)
.expect("sender entity should have Link");
link.send.push(Bytes::from_static(b"hello"));
link.send.push(Bytes::from_static(b"world"));
app.update();
let link = app
.world()
.get::<Link>(sender_entity)
.expect("sender entity should still have Link");
assert_eq!(
link.send.len(),
0,
"Drain should clear queued payloads on disconnect"
);
assert_eq!(link.state, LinkState::Unlinked);
assert!(app.world().get::<Linked>(sender_entity).is_none());
assert!(
app.world()
.get::<lightyear_link::Unlinked>(sender_entity)
.is_some()
);
drop(peer_sender);
}
#[test]
fn receive_after_peer_disconnect_unlinks_transport() {
let (client_io, server_io) = CrossbeamIo::new_pair();
let mut app = App::new();
app.add_plugins(CrossbeamPlugin);
let client_entity = app
.world_mut()
.spawn((Link::default(), Linked, client_io))
.id();
drop(server_io);
app.update();
let link = app
.world()
.get::<Link>(client_entity)
.expect("client entity should still have Link");
assert_eq!(link.state, LinkState::Unlinked);
assert!(app.world().get::<Linked>(client_entity).is_none());
assert!(
app.world()
.get::<lightyear_link::Unlinked>(client_entity)
.is_some()
);
}
#[test]
fn peer_disconnect_disconnects_raw_client() {
use lightyear_connection::prelude::client::Connect;
use lightyear_raw_connection::client::{RawClient, RawConnectionPlugin};
let (client_io, server_io) = CrossbeamIo::new_pair();
let mut app = App::new();
app.add_plugins(CrossbeamPlugin);
app.add_plugins(RawConnectionPlugin);
let client_entity = app.world_mut().spawn((RawClient, client_io)).id();
app.world_mut().trigger(Connect {
entity: client_entity,
});
app.update();
assert!(app.world().get::<Linked>(client_entity).is_some());
assert!(app.world().get::<Connected>(client_entity).is_some());
assert!(app.world().get::<Disconnected>(client_entity).is_none());
drop(server_io);
app.update();
assert!(app.world().get::<Linked>(client_entity).is_none());
assert!(app.world().get::<Connected>(client_entity).is_none());
assert!(app.world().get::<Disconnected>(client_entity).is_some());
}
#[test]
fn round_trip_send_receive() {
let (client_io, server_io) = CrossbeamIo::new_pair();
let mut app = App::new();
app.add_plugins(CrossbeamPlugin);
let client_entity = app
.world_mut()
.spawn((Link::default(), Linked, client_io))
.id();
let server_entity = app
.world_mut()
.spawn((Link::default(), Linked, server_io))
.id();
let mut client_link = app
.world_mut()
.get_mut::<Link>(client_entity)
.expect("client entity should have Link");
client_link.send.push(Bytes::from_static(b"a"));
client_link.send.push(Bytes::from_static(b"b"));
client_link.send.push(Bytes::from_static(b"c"));
app.update();
app.update();
let mut server_link = app
.world_mut()
.get_mut::<Link>(server_entity)
.expect("server entity should have Link");
let p1 = server_link.recv.pop().expect("first payload missing");
let p2 = server_link.recv.pop().expect("second payload missing");
let p3 = server_link.recv.pop().expect("third payload missing");
assert_eq!(p1.as_ref(), b"a");
assert_eq!(p2.as_ref(), b"b");
assert_eq!(p3.as_ref(), b"c");
assert!(
server_link.recv.pop().is_none(),
"no extra payloads should be received"
);
}
#[test]
fn send_with_bounded_channel_requeues_on_full() {
let (bounded_sender, _peer_recv_unread) = crossbeam_channel::bounded::<Bytes>(1);
let (_dummy_sender, dummy_recv) = crossbeam_channel::unbounded::<Bytes>();
let client_io = CrossbeamIo::new(bounded_sender, dummy_recv);
let mut app = App::new();
app.add_plugins(CrossbeamPlugin);
let client_entity = app
.world_mut()
.spawn((Link::default(), Linked, client_io))
.id();
let mut link = app
.world_mut()
.get_mut::<Link>(client_entity)
.expect("client entity should have Link");
link.send.push(Bytes::from_static(b"first"));
link.send.push(Bytes::from_static(b"second"));
link.send.push(Bytes::from_static(b"third"));
app.update();
let mut link = app
.world_mut()
.get_mut::<Link>(client_entity)
.expect("client entity should still have Link");
assert_eq!(
link.send.len(),
2,
"Full payload should be re-queued (queue starts at 3, 1 sent, 1 Full re-queued)"
);
assert_eq!(
link.send
.pop()
.expect("re-queued Full payload missing")
.as_ref(),
b"second",
"Full payload should land at front of queue to preserve FIFO"
);
assert_eq!(
link.send.pop().expect("third payload missing").as_ref(),
b"third",
"still-queued payloads should follow the re-queued one"
);
}
#[test]
fn server_mirror_via_link_start_reaches_connected() {
use lightyear_connection::prelude::Connected;
use lightyear_connection::prelude::server::ClientOf;
use lightyear_link::prelude::server::LinkOf;
use lightyear_raw_connection::prelude::server::RawServer;
let (_client_io, server_io) = CrossbeamIo::new_pair();
let mut app = App::new();
app.add_plugins(CrossbeamPlugin);
app.add_plugins(lightyear_raw_connection::server::RawConnectionPlugin);
let server_entity = app.world_mut().spawn(RawServer).id();
let mirror_entity = app
.world_mut()
.spawn((
LinkOf {
server: server_entity,
},
Link::default(),
server_io,
))
.id();
app.world_mut().trigger(LinkStart {
entity: mirror_entity,
});
app.update();
let world = app.world();
assert!(world.get::<Linked>(mirror_entity).is_some(), "Linked");
assert!(world.get::<Connected>(mirror_entity).is_some(), "Connected");
assert!(world.get::<ClientOf>(mirror_entity).is_some(), "ClientOf");
}
}