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