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 {
76        auto_reconnect_delay_res.as_ref().map(|r| r.delay)
77    }
78}
79
80pub fn rejoin_after_delay(
81    mut commands: Commands,
82    mut join_events: EventWriter<StartJoinServerEvent>,
83    query: Query<(
84        Entity,
85        &InternalReconnectAfter,
86        &Account,
87        &ConnectOpts,
88        Option<&LocalPlayerEvents>,
89    )>,
90) {
91    for (entity, reconnect_after, account, connect_opts, local_player_events) in query.iter() {
92        if Instant::now() >= reconnect_after.instant {
93            // don't keep trying to reconnect
94            commands.entity(entity).remove::<InternalReconnectAfter>();
95
96            // our Entity will be reused since the account has the same uuid
97            join_events.write(StartJoinServerEvent {
98                account: account.clone(),
99                connect_opts: connect_opts.clone(),
100                // not actually necessary since we're reusing the same entity and LocalPlayerEvents
101                // isn't removed, but this is more readable and just in case it's changed in the
102                // future
103                event_sender: local_player_events.map(|e| e.0.clone()),
104                start_join_callback_tx: None,
105            });
106        }
107    }
108}
109
110/// A resource *and* component that indicates how long to wait before
111/// reconnecting when we're kicked.
112///
113/// Initially, it's a resource in the ECS set to 5 seconds. You can modify
114/// the resource to update the global reconnect delay, or insert it as a
115/// component to set the individual delay for a single client.
116///
117/// You can also remove this resource from the ECS to disable the default
118/// auto-reconnecting behavior. Inserting the resource/component again will not
119/// make clients that were already disconnected automatically reconnect.
120#[derive(Resource, Component, Debug, Clone)]
121pub struct AutoReconnectDelay {
122    pub delay: Duration,
123}
124impl AutoReconnectDelay {
125    pub fn new(delay: Duration) -> Self {
126        Self { delay }
127    }
128}
129
130/// This is inserted when we're disconnected and indicates when we'll reconnect.
131///
132/// This is set based on [`AutoReconnectDelay`].
133#[derive(Component, Debug, Clone)]
134pub struct InternalReconnectAfter {
135    pub instant: Instant,
136}