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 azalea_world::MinecraftEntityId;
6use bevy_app::{App, Plugin, PostUpdate};
7use bevy_ecs::prelude::*;
8use derive_more::Deref;
9use tracing::info;
10
11use super::login::IsAuthenticated;
12use crate::{InstanceHolder, chat_signing, client::JoinedClientBundle, connection::RawConnection};
13
14pub struct DisconnectPlugin;
15impl Plugin for DisconnectPlugin {
16    fn build(&self, app: &mut App) {
17        app.add_event::<DisconnectEvent>().add_systems(
18            PostUpdate,
19            (
20                update_read_packets_task_running_component,
21                remove_components_from_disconnected_players,
22                // this happens after `remove_components_from_disconnected_players` since that
23                // system removes `IsConnectionAlive`, which ensures that
24                // `DisconnectEvent` won't get called again from
25                // `disconnect_on_connection_dead`
26                disconnect_on_connection_dead,
27            )
28                .chain(),
29        );
30    }
31}
32
33/// An event sent when a client got disconnected from the server.
34///
35/// If the client was kicked with a reason, that reason will be present in the
36/// [`reason`](DisconnectEvent::reason) field.
37///
38/// This event won't be sent if creating the initial connection to the server
39/// failed, for that see [`ConnectionFailedEvent`].
40///
41/// [`ConnectionFailedEvent`]: crate::join::ConnectionFailedEvent
42
43#[derive(Event)]
44pub struct DisconnectEvent {
45    pub entity: Entity,
46    pub reason: Option<FormattedText>,
47}
48
49/// A bundle of components that are removed when a client disconnects.
50///
51/// This shouldn't be used for inserts because not all of the components should
52/// always be present.
53#[derive(Bundle)]
54pub struct RemoveOnDisconnectBundle {
55    pub joined_client: JoinedClientBundle,
56    pub entity: EntityBundle,
57    pub minecraft_entity_id: MinecraftEntityId,
58    pub instance_holder: InstanceHolder,
59    pub player_metadata: PlayerMetadataBundle,
60    pub in_loaded_chunk: InLoadedChunk,
61    //// This makes it close the TCP connection.
62    pub raw_connection: RawConnection,
63    /// This makes it not send [`DisconnectEvent`] again.
64    pub is_connection_alive: IsConnectionAlive,
65    /// Resend our chat signing certs next time.
66    pub chat_signing_session: chat_signing::ChatSigningSession,
67    /// They're not authenticated anymore if they disconnected.
68    pub is_authenticated: IsAuthenticated,
69}
70
71/// A system that removes the several components from our clients when they get
72/// a [`DisconnectEvent`].
73pub fn remove_components_from_disconnected_players(
74    mut commands: Commands,
75    mut events: EventReader<DisconnectEvent>,
76    mut loaded_by_query: Query<&mut azalea_entity::LoadedBy>,
77) {
78    for DisconnectEvent { entity, reason } in events.read() {
79        info!(
80            "A client {entity:?} was disconnected{}",
81            if let Some(reason) = reason {
82                format!(": {reason}")
83            } else {
84                "".to_string()
85            }
86        );
87        commands
88            .entity(*entity)
89            .remove::<RemoveOnDisconnectBundle>();
90        // note that we don't remove the client from the ECS, so if they decide
91        // to reconnect they'll keep their state
92
93        // now we have to remove ourselves from the LoadedBy for every entity.
94        // in theory this could be inefficient if we have massive swarms... but in
95        // practice this is fine.
96        for mut loaded_by in &mut loaded_by_query.iter_mut() {
97            loaded_by.remove(entity);
98        }
99    }
100}
101
102#[derive(Component, Clone, Copy, Debug, Deref)]
103pub struct IsConnectionAlive(bool);
104
105fn update_read_packets_task_running_component(
106    query: Query<(Entity, &RawConnection)>,
107    mut commands: Commands,
108) {
109    for (entity, raw_connection) in &query {
110        let running = raw_connection.is_alive();
111        commands.entity(entity).insert(IsConnectionAlive(running));
112    }
113}
114
115#[allow(clippy::type_complexity)]
116fn disconnect_on_connection_dead(
117    query: Query<(Entity, &IsConnectionAlive), (Changed<IsConnectionAlive>, With<LocalEntity>)>,
118    mut disconnect_events: EventWriter<DisconnectEvent>,
119) {
120    for (entity, &is_connection_alive) in &query {
121        if !*is_connection_alive {
122            disconnect_events.write(DisconnectEvent {
123                entity,
124                reason: None,
125            });
126        }
127    }
128}