use {
crate::connection::Disconnect,
bevy_app::prelude::*,
bevy_ecs::prelude::*,
bevy_hierarchy::{Children, DespawnRecursiveExt},
bevy_reflect::prelude::*,
tracing::debug,
web_time::Instant,
};
#[derive(Debug)]
pub(crate) struct ServerPlugin;
impl Plugin for ServerPlugin {
fn build(&self, app: &mut App) {
app.register_type::<Server>()
.register_type::<Opened>()
.observe(on_opening)
.observe(on_opened)
.observe(on_close)
.observe(on_closed);
}
}
#[derive(Debug, Clone, Copy, Default, Component, Reflect)]
#[reflect(Component)]
pub struct Server;
#[derive(Debug, Clone, Copy, Component, Reflect)]
#[reflect(Component)]
pub struct Opened {
pub at: Instant,
}
impl Opened {
#[must_use]
pub fn now() -> Self {
Self { at: Instant::now() }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Event)]
pub struct Close {
pub reason: String,
}
impl Close {
#[must_use]
pub fn new(reason: impl Into<String>) -> Self {
Self {
reason: reason.into(),
}
}
}
#[derive(Debug, Event)]
pub struct Closed {
pub reason: CloseReason<anyhow::Error>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CloseReason<E> {
User(String),
Error(E),
}
impl<E> CloseReason<E> {
pub fn map_err<F>(self, f: impl FnOnce(E) -> F) -> CloseReason<F> {
match self {
Self::User(reason) => CloseReason::User(reason),
Self::Error(err) => CloseReason::Error(f(err)),
}
}
}
impl<E> From<E> for CloseReason<E> {
fn from(value: E) -> Self {
Self::Error(value)
}
}
fn on_opening(trigger: Trigger<OnAdd, Server>) {
let server = trigger.entity();
debug!("{server} opening");
}
fn on_opened(trigger: Trigger<OnAdd, Opened>) {
let server = trigger.entity();
debug!("{server} opened");
}
fn on_close(trigger: Trigger<Close>, mut commands: Commands) {
let server = trigger.entity();
let reason = CloseReason::User(trigger.event().reason.clone());
commands.trigger_targets(Closed { reason }, server);
}
fn on_closed(trigger: Trigger<Closed>, children: Query<&Children>, mut commands: Commands) {
let server = trigger.entity();
let children = children
.get(server)
.map(|children| children.iter().copied().collect::<Vec<_>>())
.unwrap_or_default();
match &trigger.event().reason {
CloseReason::User(reason) => {
debug!("{server} closed by user: {reason}");
commands.trigger_targets(Disconnect::new(reason), children);
}
CloseReason::Error(err) => {
debug!("{server} closed due to error: {err:#}");
}
}
if let Some(server) = commands.get_entity(server) {
server.despawn_recursive();
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::{
AeronetIoPlugin,
connection::{DisconnectReason, Disconnected},
},
bevy_hierarchy::BuildWorldChildren,
};
#[test]
fn disconnect_clients_on_close() {
const REASON: &str = "disconnect reason";
#[derive(Resource)]
struct HasClosed(bool);
#[derive(Resource)]
struct HasDisconnected(bool);
let mut app = App::new();
app.add_plugins(AeronetIoPlugin)
.insert_resource(HasClosed(false))
.insert_resource(HasDisconnected(false));
let client = app.world_mut().spawn_empty().id();
app.world_mut().entity_mut(client).observe(
|trigger: Trigger<Disconnected>, mut has_disconnected: ResMut<HasDisconnected>| {
assert!(matches!(
&trigger.event().reason,
DisconnectReason::User(reason) if reason == REASON
));
has_disconnected.0 = true;
},
);
let server = app.world_mut().spawn_empty().id();
app.world_mut()
.entity_mut(server)
.add_child(client)
.observe(
|trigger: Trigger<Closed>, mut has_closed: ResMut<HasClosed>| {
assert!(matches!(
&trigger.event().reason,
CloseReason::User(reason) if reason == REASON
));
has_closed.0 = true;
},
);
app.world_mut().trigger_targets(Close::new(REASON), server);
app.update();
assert!(app.world().get_entity(client).is_none());
assert!(app.world().resource::<HasDisconnected>().0);
assert!(app.world().get_entity(server).is_none());
assert!(app.world().resource::<HasClosed>().0);
}
}