aboutsummaryrefslogtreecommitdiff
path: root/azalea-client/src/plugins/disconnect.rs
blob: 478023ea111d4625ade84e80e7568403819cbe9e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
//! Disconnect a client from the server.

use azalea_chat::FormattedText;
use azalea_core::entity_id::MinecraftEntityId;
use azalea_entity::{
    EntityBundle, HasClientLoaded, InLoadedChunk, LocalEntity, metadata::PlayerMetadataBundle,
};
use bevy_app::{App, Plugin, PostUpdate};
use bevy_ecs::prelude::*;
use derive_more::Deref;
use tracing::info;

use super::login::IsAuthenticated;
#[cfg(feature = "online-mode")]
use crate::chat_signing;
use crate::{
    client::JoinedClientBundle, connection::RawConnection, local_player::WorldHolder, mining,
    tick_counter::TicksConnected,
};

pub struct DisconnectPlugin;
impl Plugin for DisconnectPlugin {
    fn build(&self, app: &mut App) {
        app.add_message::<DisconnectEvent>().add_systems(
            PostUpdate,
            (
                update_read_packets_task_running_component,
                remove_components_from_disconnected_players,
                // this happens after `remove_components_from_disconnected_players` since that
                // system removes `IsConnectionAlive`, which ensures that
                // `DisconnectEvent` won't get called again from
                // `disconnect_on_connection_dead`
                disconnect_on_connection_dead,
            )
                .chain(),
        );
    }
}

/// An event sent when a client got disconnected from the server.
///
/// If the client was kicked with a reason, that reason will be present in the
/// [`reason`](DisconnectEvent::reason) field.
///
/// This event won't be sent if creating the initial connection to the server
/// failed, for that see [`ConnectionFailedEvent`].
///
/// [`ConnectionFailedEvent`]: crate::join::ConnectionFailedEvent
#[derive(Message)]
pub struct DisconnectEvent {
    pub entity: Entity,
    pub reason: Option<FormattedText>,
}

/// A bundle of components that are removed when a client disconnects.
///
/// This shouldn't be used for inserts because not all of the components should
/// always be present.
#[derive(Bundle)]
pub struct RemoveOnDisconnectBundle {
    pub joined_client: JoinedClientBundle,

    pub entity: EntityBundle,
    pub minecraft_entity_id: MinecraftEntityId,
    pub world_holder: WorldHolder,
    pub player_metadata: PlayerMetadataBundle,
    pub in_loaded_chunk: InLoadedChunk,
    //// This makes it close the TCP connection.
    pub raw_connection: RawConnection,
    /// This makes it not send [`DisconnectEvent`] again.
    pub is_connection_alive: IsConnectionAlive,
    /// Resend our chat signing certs next time.
    #[cfg(feature = "online-mode")]
    pub chat_signing_session: chat_signing::ChatSigningSession,
    /// They're not authenticated anymore if they disconnected.
    pub is_authenticated: IsAuthenticated,
    // send ServerboundPlayerLoaded next time we join.
    pub has_client_loaded: HasClientLoaded,
    // TickCounter is reset on reconnect
    pub ticks_alive: TicksConnected,

    // the rest of the mining components are already removed, as JoinedClientBundle includes
    // MineBundle
    pub mining: mining::Mining,
}

/// A system that removes the several components from our clients when they get
/// a [`DisconnectEvent`].
pub fn remove_components_from_disconnected_players(
    mut commands: Commands,
    mut events: MessageReader<DisconnectEvent>,
    mut loaded_by_query: Query<&mut azalea_entity::LoadedBy>,
) {
    for DisconnectEvent { entity, reason } in events.read() {
        info!(
            "A client {entity:?} was disconnected{}",
            if let Some(reason) = reason {
                format!(": {reason}")
            } else {
                "".to_owned()
            }
        );
        commands
            .entity(*entity)
            .remove::<RemoveOnDisconnectBundle>();
        // note that we don't remove the client from the ECS, so if they decide
        // to reconnect they'll keep their state

        // now we have to remove ourselves from the LoadedBy for every entity.
        // in theory this could be inefficient if we have massive swarms... but in
        // practice this is fine.
        for mut loaded_by in &mut loaded_by_query.iter_mut() {
            loaded_by.remove(entity);
        }
    }
}

#[derive(Clone, Component, Copy, Debug, Deref)]
pub struct IsConnectionAlive(bool);

fn update_read_packets_task_running_component(
    query: Query<(Entity, &RawConnection)>,
    mut commands: Commands,
) {
    for (entity, raw_connection) in &query {
        let running = raw_connection.is_alive();
        commands.entity(entity).insert(IsConnectionAlive(running));
    }
}

#[allow(clippy::type_complexity)]
fn disconnect_on_connection_dead(
    query: Query<(Entity, &IsConnectionAlive), (Changed<IsConnectionAlive>, With<LocalEntity>)>,
    mut disconnect_events: MessageWriter<DisconnectEvent>,
) {
    for (entity, &is_connection_alive) in &query {
        if !*is_connection_alive {
            disconnect_events.write(DisconnectEvent {
                entity,
                reason: None,
            });
        }
    }
}