azalea_client/plugins/
disconnect.rs1use 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 disconnect_on_connection_dead,
34 )
35 .chain(),
36 );
37 }
38}
39
40#[derive(Message)]
51pub struct DisconnectEvent {
52 pub entity: Entity,
53 pub reason: Option<FormattedText>,
54}
55
56#[derive(Bundle)]
61pub struct RemoveOnDisconnectBundle {
62 pub joined_client: JoinedClientBundle,
63
64 pub entity: EntityBundle,
65 pub minecraft_entity_id: MinecraftEntityId,
66 pub world_holder: WorldHolder,
67 pub player_metadata: PlayerMetadataBundle,
68 pub in_loaded_chunk: InLoadedChunk,
69 pub raw_connection: RawConnection,
71 pub is_connection_alive: IsConnectionAlive,
73 #[cfg(feature = "online-mode")]
75 pub chat_signing_session: chat_signing::ChatSigningSession,
76 pub is_authenticated: IsAuthenticated,
78 pub has_client_loaded: HasClientLoaded,
80 pub ticks_alive: TicksConnected,
82
83 pub mining: mining::Mining,
86}
87
88pub fn remove_components_from_disconnected_players(
91 mut commands: Commands,
92 mut events: MessageReader<DisconnectEvent>,
93 mut loaded_by_query: Query<&mut azalea_entity::LoadedBy>,
94) {
95 for DisconnectEvent { entity, reason } in events.read() {
96 info!(
97 "A client {entity:?} was disconnected{}",
98 if let Some(reason) = reason {
99 format!(": {reason}")
100 } else {
101 "".to_owned()
102 }
103 );
104 commands
105 .entity(*entity)
106 .remove::<RemoveOnDisconnectBundle>();
107 for mut loaded_by in &mut loaded_by_query.iter_mut() {
114 loaded_by.remove(entity);
115 }
116 }
117}
118
119#[derive(Clone, Component, Copy, Debug, Deref)]
120pub struct IsConnectionAlive(bool);
121
122fn update_read_packets_task_running_component(
123 query: Query<(Entity, &RawConnection)>,
124 mut commands: Commands,
125) {
126 for (entity, raw_connection) in &query {
127 let running = raw_connection.is_alive();
128 commands.entity(entity).insert(IsConnectionAlive(running));
129 }
130}
131
132#[allow(clippy::type_complexity)]
133fn disconnect_on_connection_dead(
134 query: Query<(Entity, &IsConnectionAlive), (Changed<IsConnectionAlive>, With<LocalEntity>)>,
135 mut disconnect_events: MessageWriter<DisconnectEvent>,
136) {
137 for (entity, &is_connection_alive) in &query {
138 if !*is_connection_alive {
139 disconnect_events.write(DisconnectEvent {
140 entity,
141 reason: None,
142 });
143 }
144 }
145}