use {
crate::{Endpoint, Session},
bevy_app::prelude::*,
bevy_derive::Deref,
bevy_ecs::prelude::*,
bevy_hierarchy::DespawnRecursiveExt,
core::{fmt::Debug, net::SocketAddr},
tracing::debug,
};
#[derive(Debug)]
pub(crate) struct ConnectionPlugin;
impl Plugin for ConnectionPlugin {
fn build(&self, app: &mut App) {
app.observe(on_connecting)
.observe(on_connected)
.observe(on_disconnect)
.observe(on_disconnected);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Event)]
pub struct Disconnect {
pub reason: String,
}
impl Disconnect {
#[must_use]
pub fn new(reason: impl Into<String>) -> Self {
Self {
reason: reason.into(),
}
}
}
#[derive(Debug, Event)]
pub struct Disconnected {
pub reason: DisconnectReason<anyhow::Error>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DisconnectReason<E> {
User(String),
Peer(String),
Error(E),
}
impl<E> DisconnectReason<E> {
pub fn map_err<F>(self, f: impl FnOnce(E) -> F) -> DisconnectReason<F> {
match self {
Self::User(reason) => DisconnectReason::User(reason),
Self::Peer(reason) => DisconnectReason::Peer(reason),
Self::Error(err) => DisconnectReason::Error(f(err)),
}
}
}
impl<E> From<E> for DisconnectReason<E> {
fn from(value: E) -> Self {
Self::Error(value)
}
}
pub const DROP_DISCONNECT_REASON: &str = "dropped";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deref, Component)]
pub struct LocalAddr(pub SocketAddr);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deref, Component)]
pub struct PeerAddr(pub SocketAddr);
fn on_connecting(trigger: Trigger<OnAdd, Endpoint>) {
let entity = trigger.entity();
debug!("{entity} connecting");
}
fn on_connected(trigger: Trigger<OnAdd, Session>) {
let entity = trigger.entity();
debug!("{entity} connected");
}
fn on_disconnect(trigger: Trigger<Disconnect>, mut commands: Commands) {
let entity = trigger.entity();
let reason = DisconnectReason::User(trigger.event().reason.clone());
commands.trigger_targets(Disconnected { reason }, entity);
}
fn on_disconnected(trigger: Trigger<Disconnected>, mut commands: Commands) {
let entity = trigger.entity();
match &trigger.event().reason {
DisconnectReason::User(reason) => {
debug!("{entity} disconnected by user: {reason}");
}
DisconnectReason::Peer(reason) => {
debug!("{entity} disconnected by user: {reason}");
}
DisconnectReason::Error(err) => {
debug!("{entity} disconnected due to error: {err:#}");
}
}
if let Some(entity) = commands.get_entity(entity) {
entity.despawn_recursive();
}
}
#[cfg(test)]
mod tests {
use {super::*, crate::AeronetIoPlugin};
#[test]
fn remove_entity_on_disconnect() {
const REASON: &str = "disconnect reason";
#[derive(Resource)]
struct HasDisconnected(bool);
let mut app = App::new();
app.add_plugins(AeronetIoPlugin)
.insert_resource(HasDisconnected(false));
let entity = app.world_mut().spawn_empty().id();
app.world_mut().entity_mut(entity).observe(
|trigger: Trigger<Disconnected>, mut has_disconnected: ResMut<HasDisconnected>| {
assert!(matches!(
&trigger.event().reason,
DisconnectReason::User(reason) if reason == REASON
));
has_disconnected.0 = true;
},
);
app.world_mut()
.trigger_targets(Disconnect::new(REASON), entity);
app.update();
assert!(app.world().get_entity(entity).is_none());
assert!(app.world().resource::<HasDisconnected>().0);
}
}