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
|
//! Disconnect a client from the server.
use azalea_chat::FormattedText;
use azalea_entity::{EntityBundle, InLoadedChunk, LocalEntity, metadata::PlayerMetadataBundle};
use azalea_world::MinecraftEntityId;
use bevy_app::{App, Plugin, PostUpdate};
use bevy_ecs::prelude::*;
use derive_more::Deref;
use tracing::info;
use super::login::IsAuthenticated;
use crate::{InstanceHolder, chat_signing, client::JoinedClientBundle, connection::RawConnection};
pub struct DisconnectPlugin;
impl Plugin for DisconnectPlugin {
fn build(&self, app: &mut App) {
app.add_event::<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(Event)]
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 instance_holder: InstanceHolder,
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.
pub chat_signing_session: chat_signing::ChatSigningSession,
/// They're not authenticated anymore if they disconnected.
pub is_authenticated: IsAuthenticated,
}
/// 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: EventReader<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_string()
}
);
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(Component, Clone, 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: EventWriter<DisconnectEvent>,
) {
for (entity, &is_connection_alive) in &query {
if !*is_connection_alive {
disconnect_events.write(DisconnectEvent {
entity,
reason: None,
});
}
}
}
|