azalea_client/plugins/
disconnect.rs

1//! Disconnect a client from the server.
2
3use azalea_chat::FormattedText;
4use azalea_entity::{EntityBundle, InLoadedChunk, LocalEntity, metadata::PlayerMetadataBundle};
5use bevy_app::{App, Plugin, PostUpdate};
6use bevy_ecs::{
7    component::Component,
8    entity::Entity,
9    event::{EventReader, EventWriter},
10    prelude::Event,
11    query::{Changed, With},
12    schedule::IntoSystemConfigs,
13    system::{Commands, Query},
14};
15use derive_more::Deref;
16use tracing::trace;
17
18use crate::{
19    InstanceHolder, client::JoinedClientBundle, events::LocalPlayerEvents,
20    raw_connection::RawConnection,
21};
22
23pub struct DisconnectPlugin;
24impl Plugin for DisconnectPlugin {
25    fn build(&self, app: &mut App) {
26        app.add_event::<DisconnectEvent>().add_systems(
27            PostUpdate,
28            (
29                update_read_packets_task_running_component,
30                disconnect_on_connection_dead,
31                remove_components_from_disconnected_players,
32            )
33                .chain(),
34        );
35    }
36}
37
38/// An event sent when a client is getting disconnected.
39#[derive(Event)]
40pub struct DisconnectEvent {
41    pub entity: Entity,
42    pub reason: Option<FormattedText>,
43}
44
45/// A system that removes the several components from our clients when they get
46/// a [`DisconnectEvent`].
47pub fn remove_components_from_disconnected_players(
48    mut commands: Commands,
49    mut events: EventReader<DisconnectEvent>,
50    mut loaded_by_query: Query<&mut azalea_entity::LoadedBy>,
51) {
52    for DisconnectEvent { entity, .. } in events.read() {
53        trace!("Got DisconnectEvent for {entity:?}");
54        commands
55            .entity(*entity)
56            .remove::<JoinedClientBundle>()
57            .remove::<EntityBundle>()
58            .remove::<InstanceHolder>()
59            .remove::<PlayerMetadataBundle>()
60            .remove::<InLoadedChunk>()
61            // this makes it close the tcp connection
62            .remove::<RawConnection>()
63            // swarm detects when this tx gets dropped to fire SwarmEvent::Disconnect
64            .remove::<LocalPlayerEvents>();
65        // note that we don't remove the client from the ECS, so if they decide
66        // to reconnect they'll keep their state
67
68        // now we have to remove ourselves from the LoadedBy for every entity.
69        // in theory this could be inefficient if we have massive swarms... but in
70        // practice this is fine.
71        for mut loaded_by in &mut loaded_by_query.iter_mut() {
72            loaded_by.remove(entity);
73        }
74    }
75}
76
77#[derive(Component, Clone, Copy, Debug, Deref)]
78pub struct IsConnectionAlive(bool);
79
80fn update_read_packets_task_running_component(
81    query: Query<(Entity, &RawConnection)>,
82    mut commands: Commands,
83) {
84    for (entity, raw_connection) in &query {
85        let running = raw_connection.is_alive();
86        commands.entity(entity).insert(IsConnectionAlive(running));
87    }
88}
89
90#[allow(clippy::type_complexity)]
91fn disconnect_on_connection_dead(
92    query: Query<(Entity, &IsConnectionAlive), (Changed<IsConnectionAlive>, With<LocalEntity>)>,
93    mut disconnect_events: EventWriter<DisconnectEvent>,
94) {
95    for (entity, &is_connection_alive) in &query {
96        if !*is_connection_alive {
97            disconnect_events.send(DisconnectEvent {
98                entity,
99                reason: None,
100            });
101        }
102    }
103}