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