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