azalea_client/plugins/
auto_reconnect.rs

1//! Auto-reconnect to the server when the client is kicked.
2//!
3//! See [`AutoReconnectPlugin`] for more information.
4
5use std::time::{Duration, Instant};
6
7use bevy_app::prelude::*;
8use bevy_ecs::prelude::*;
9
10use super::{
11    disconnect::DisconnectEvent,
12    events::LocalPlayerEvents,
13    join::{ConnectOpts, ConnectionFailedEvent, StartJoinServerEvent},
14};
15use crate::Account;
16
17/// The default delay that Azalea will use for reconnecting our clients. See
18/// [`AutoReconnectPlugin`] for more information.
19pub const DEFAULT_RECONNECT_DELAY: Duration = Duration::from_secs(5);
20
21/// A default plugin that makes clients automatically rejoin the server when
22/// they're disconnected. The reconnect delay is configurable globally or
23/// per-client with the [`AutoReconnectDelay`] resource/component. Auto
24/// reconnecting can be disabled by removing the resource from the ECS.
25///
26/// The delay defaults to [`DEFAULT_RECONNECT_DELAY`].
27pub struct AutoReconnectPlugin;
28impl Plugin for AutoReconnectPlugin {
29    fn build(&self, app: &mut App) {
30        app.insert_resource(AutoReconnectDelay::new(DEFAULT_RECONNECT_DELAY))
31            .add_systems(
32                Update,
33                (start_rejoin_on_disconnect, rejoin_after_delay)
34                    .chain()
35                    .before(super::join::handle_start_join_server_event),
36            );
37    }
38}
39
40pub fn start_rejoin_on_disconnect(
41    mut commands: Commands,
42    mut disconnect_events: EventReader<DisconnectEvent>,
43    mut connection_failed_events: EventReader<ConnectionFailedEvent>,
44    auto_reconnect_delay_res: Option<Res<AutoReconnectDelay>>,
45    auto_reconnect_delay_query: Query<&AutoReconnectDelay>,
46) {
47    for entity in disconnect_events
48        .read()
49        .map(|e| e.entity)
50        .chain(connection_failed_events.read().map(|e| e.entity))
51    {
52        let Some(delay) = get_delay(
53            &auto_reconnect_delay_res,
54            auto_reconnect_delay_query,
55            entity,
56        ) else {
57            // no auto reconnect
58            continue;
59        };
60
61        let reconnect_after = Instant::now() + delay;
62        commands.entity(entity).insert(InternalReconnectAfter {
63            instant: reconnect_after,
64        });
65    }
66}
67
68fn get_delay(
69    auto_reconnect_delay_res: &Option<Res<AutoReconnectDelay>>,
70    auto_reconnect_delay_query: Query<&AutoReconnectDelay>,
71    entity: Entity,
72) -> Option<Duration> {
73    if let Ok(c) = auto_reconnect_delay_query.get(entity) {
74        Some(c.delay)
75    } else if let Some(r) = &auto_reconnect_delay_res {
76        Some(r.delay)
77    } else {
78        None
79    }
80}
81
82pub fn rejoin_after_delay(
83    mut commands: Commands,
84    mut join_events: EventWriter<StartJoinServerEvent>,
85    query: Query<(
86        Entity,
87        &InternalReconnectAfter,
88        &Account,
89        &ConnectOpts,
90        Option<&LocalPlayerEvents>,
91    )>,
92) {
93    for (entity, reconnect_after, account, connect_opts, local_player_events) in query.iter() {
94        if Instant::now() >= reconnect_after.instant {
95            // don't keep trying to reconnect
96            commands.entity(entity).remove::<InternalReconnectAfter>();
97
98            // our Entity will be reused since the account has the same uuid
99            join_events.write(StartJoinServerEvent {
100                account: account.clone(),
101                connect_opts: connect_opts.clone(),
102                // not actually necessary since we're reusing the same entity and LocalPlayerEvents
103                // isn't removed, but this is more readable and just in case it's changed in the
104                // future
105                event_sender: local_player_events.map(|e| e.0.clone()),
106                start_join_callback_tx: None,
107            });
108        }
109    }
110}
111
112/// A resource *and* component that indicates how long to wait before
113/// reconnecting when we're kicked.
114///
115/// Initially, it's a resource in the ECS set to 5 seconds. You can modify
116/// the resource to update the global reconnect delay, or insert it as a
117/// component to set the individual delay for a single client.
118///
119/// You can also remove this resource from the ECS to disable the default
120/// auto-reconnecting behavior. Inserting the resource/component again will not
121/// make clients that were already disconnected automatically reconnect.
122#[derive(Resource, Component, Debug, Clone)]
123pub struct AutoReconnectDelay {
124    pub delay: Duration,
125}
126impl AutoReconnectDelay {
127    pub fn new(delay: Duration) -> Self {
128        Self { delay }
129    }
130}
131
132/// This is inserted when we're disconnected and indicates when we'll reconnect.
133///
134/// This is set based on [`AutoReconnectDelay`].
135#[derive(Component, Debug, Clone)]
136pub struct InternalReconnectAfter {
137    pub instant: Instant,
138}