azalea_client/plugins/
disconnect.rs

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