use {
crate::{Session, SessionEndpoint},
alloc::string::String,
bevy_app::prelude::*,
bevy_ecs::prelude::*,
core::{fmt::Debug, net::SocketAddr},
derive_more::Deref,
log::debug,
};
pub(crate) struct ConnectionPlugin;
impl Plugin for ConnectionPlugin {
fn build(&self, app: &mut App) {
app.add_observer(on_connecting)
.add_observer(on_connected)
.add_observer(on_disconnect)
.add_observer(on_disconnected);
}
}
#[derive(Debug, Clone, PartialEq, Eq, EntityEvent)]
pub struct Disconnect {
pub entity: Entity,
pub reason: String,
}
impl Disconnect {
#[must_use]
pub fn new(entity: Entity, reason: impl Into<String>) -> Self {
Self {
entity,
reason: reason.into(),
}
}
}
#[derive(Debug, EntityEvent)]
pub struct Disconnected {
pub entity: Entity,
pub reason: DisconnectReason,
}
#[derive(Debug)]
pub enum DisconnectReason {
ByUser(String),
ByPeer(String),
ByError(anyhow::Error),
}
impl DisconnectReason {
#[must_use]
pub fn by_user(reason: impl Into<String>) -> Self {
Self::ByUser(reason.into())
}
#[must_use]
pub fn by_peer(reason: impl Into<String>) -> Self {
Self::ByPeer(reason.into())
}
#[must_use]
pub fn by_error(reason: impl Into<anyhow::Error>) -> Self {
Self::ByError(reason.into())
}
#[must_use]
pub fn map_err(self, f: impl FnOnce(anyhow::Error) -> anyhow::Error) -> Self {
match self {
Self::ByUser(reason) => Self::ByUser(reason),
Self::ByPeer(reason) => Self::ByPeer(reason),
Self::ByError(err) => Self::ByError(f(err)),
}
}
}
impl<E: Into<anyhow::Error>> From<E> for DisconnectReason {
fn from(value: E) -> Self {
Self::by_error(value)
}
}
pub const DROP_DISCONNECT_REASON: &str = "(dropped)";
pub const UNKNOWN_DISCONNECT_REASON: &str = "(unknown)";
#[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: On<Add, SessionEndpoint>) {
let entity = trigger.event_target();
debug!("{entity} connecting");
}
fn on_connected(trigger: On<Add, Session>) {
let entity = trigger.event_target();
debug!("{entity} connected");
}
fn on_disconnect(trigger: On<Disconnect>, mut commands: Commands) {
let entity = trigger.event_target();
commands.trigger(Disconnected {
entity,
reason: DisconnectReason::by_user(&trigger.reason),
});
}
fn on_disconnected(trigger: On<Disconnected>, mut commands: Commands) {
let entity = trigger.event_target();
match &trigger.reason {
DisconnectReason::ByUser(reason) => {
debug!("{entity} disconnected by user: {reason}");
}
DisconnectReason::ByPeer(reason) => {
debug!("{entity} disconnected by peer: {reason}");
}
DisconnectReason::ByError(err) => {
debug!("{entity} disconnected due to error: {err:#}");
}
}
commands.entity(entity).try_despawn();
}
#[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: On<Disconnected>, mut has_disconnected: ResMut<HasDisconnected>| {
assert!(matches!(
&trigger.reason,
DisconnectReason::ByUser(reason) if reason == REASON
));
has_disconnected.0 = true;
},
);
app.world_mut().trigger(Disconnect::new(entity, REASON));
app.update();
assert!(app.world().get_entity(entity).is_err());
assert!(app.world().resource::<HasDisconnected>().0);
}
}