Skip to main content

azalea_client/plugins/
disconnect.rs

1//! Disconnect a client from the server.
2
3use azalea_chat::FormattedText;
4use azalea_core::entity_id::MinecraftEntityId;
5use azalea_entity::{
6    EntityBundle, HasClientLoaded, InLoadedChunk, LocalEntity, metadata::PlayerMetadataBundle,
7};
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::WorldHolder, mining,
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#[derive(Message)]
50pub struct DisconnectEvent {
51    pub entity: Entity,
52    pub reason: Option<FormattedText>,
53}
54
55/// A bundle of components that are removed when a client disconnects.
56///
57/// This shouldn't be used for inserts because not all of the components should
58/// always be present.
59#[derive(Bundle)]
60pub struct RemoveOnDisconnectBundle {
61    pub joined_client: JoinedClientBundle,
62
63    pub entity: EntityBundle,
64    pub minecraft_entity_id: MinecraftEntityId,
65    pub world_holder: WorldHolder,
66    pub player_metadata: PlayerMetadataBundle,
67    pub in_loaded_chunk: InLoadedChunk,
68    //// This makes it close the TCP connection.
69    pub raw_connection: RawConnection,
70    /// This makes it not send [`DisconnectEvent`] again.
71    pub is_connection_alive: IsConnectionAlive,
72    /// Resend our chat signing certs next time.
73    #[cfg(feature = "online-mode")]
74    pub chat_signing_session: chat_signing::ChatSigningSession,
75    /// They're not authenticated anymore if they disconnected.
76    pub is_authenticated: IsAuthenticated,
77    // send ServerboundPlayerLoaded next time we join.
78    pub has_client_loaded: HasClientLoaded,
79    // TickCounter is reset on reconnect
80    pub ticks_alive: TicksConnected,
81
82    // the rest of the mining components are already removed, as JoinedClientBundle includes
83    // MineBundle
84    pub mining: mining::Mining,
85}
86
87/// A system that removes the several components from our clients when they get
88/// a [`DisconnectEvent`].
89pub fn remove_components_from_disconnected_players(
90    mut commands: Commands,
91    mut events: MessageReader<DisconnectEvent>,
92    mut loaded_by_query: Query<&mut azalea_entity::LoadedBy>,
93) {
94    for DisconnectEvent { entity, reason } in events.read() {
95        info!(
96            "A client {entity:?} was disconnected{}",
97            if let Some(reason) = reason {
98                format!(": {reason}")
99            } else {
100                "".to_owned()
101            }
102        );
103        commands
104            .entity(*entity)
105            .remove::<RemoveOnDisconnectBundle>();
106        // note that we don't remove the client from the ECS, so if they decide
107        // to reconnect they'll keep their state
108
109        // now we have to remove ourselves from the LoadedBy for every entity.
110        // in theory this could be inefficient if we have massive swarms... but in
111        // practice this is fine.
112        for mut loaded_by in &mut loaded_by_query.iter_mut() {
113            loaded_by.remove(entity);
114        }
115    }
116}
117
118#[derive(Clone, Component, Copy, Debug, Deref)]
119pub struct IsConnectionAlive(bool);
120
121fn update_read_packets_task_running_component(
122    query: Query<(Entity, &RawConnection)>,
123    mut commands: Commands,
124) {
125    for (entity, raw_connection) in &query {
126        let running = raw_connection.is_alive();
127        commands.entity(entity).insert(IsConnectionAlive(running));
128    }
129}
130
131#[allow(clippy::type_complexity)]
132fn disconnect_on_connection_dead(
133    query: Query<(Entity, &IsConnectionAlive), (Changed<IsConnectionAlive>, With<LocalEntity>)>,
134    mut disconnect_events: MessageWriter<DisconnectEvent>,
135) {
136    for (entity, &is_connection_alive) in &query {
137        if !*is_connection_alive {
138            disconnect_events.write(DisconnectEvent {
139                entity,
140                reason: None,
141            });
142        }
143    }
144}