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