azalea/
builder.rs

1use std::time::Duration;
2
3use azalea_client::{Account, DefaultPlugins};
4use azalea_protocol::address::ResolvableAddr;
5use bevy_app::{AppExit, Plugins};
6use bevy_ecs::component::Component;
7
8use crate::{
9    HandleFn, JoinOpts, NoState,
10    bot::DefaultBotPlugins,
11    swarm::{self, SwarmBuilder},
12};
13
14/// A builder for creating new [`Client`](crate::Client)s. This is the
15/// recommended way of making a bot.
16///
17/// ```no_run
18/// # use azalea::prelude::*;
19/// # #[tokio::main]
20/// # async fn main() {
21/// ClientBuilder::new()
22///     .set_handler(handle)
23///     .start(Account::offline("bot"), "localhost")
24///     .await;
25/// # }
26/// # #[derive(Clone, Component, Default)]
27/// # pub struct State;
28/// # async fn handle(mut bot: Client, event: Event, state: State) -> anyhow::Result<()> {
29/// #     Ok(())
30/// # }
31/// ```
32pub struct ClientBuilder<S, R>
33where
34    S: Default + Send + Sync + Clone + Component + 'static,
35    R: Send + 'static,
36    Self: Send,
37{
38    /// Internally, ClientBuilder is just a wrapper over SwarmBuilder since it's
39    /// technically just a subset of it so we can avoid duplicating code this
40    /// way.
41    swarm: SwarmBuilder<S, swarm::NoSwarmState, R, ()>,
42}
43impl ClientBuilder<NoState, ()> {
44    /// Start building a client that can join the world.
45    #[must_use]
46    pub fn new() -> Self {
47        Self::new_without_plugins()
48            .add_plugins(DefaultPlugins)
49            .add_plugins(DefaultBotPlugins)
50    }
51
52    /// [`Self::new`] but without adding the plugins by default.
53    ///
54    /// This is useful if you want to disable a default plugin. This also exists
55    /// for swarms, see [`SwarmBuilder::new_without_plugins`].
56    ///
57    /// Note that you can also disable `LogPlugin` by disabling the `log`
58    /// feature.
59    ///
60    /// You **must** add [`DefaultPlugins`] and [`DefaultBotPlugins`] to this.
61    ///
62    /// ```
63    /// # use azalea::prelude::*;
64    /// use azalea::app::PluginGroup;
65    ///
66    /// let client_builder = ClientBuilder::new_without_plugins()
67    ///     .add_plugins(
68    ///         azalea::DefaultPlugins
69    ///             .build()
70    ///             .disable::<azalea::chat_signing::ChatSigningPlugin>(),
71    ///     )
72    ///     .add_plugins(azalea::bot::DefaultBotPlugins);
73    /// # client_builder.set_handler(handle);
74    /// # #[derive(Clone, Component, Default)]
75    /// # pub struct State;
76    /// # async fn handle(mut bot: Client, event: Event, state: State) -> anyhow::Result<()> {
77    /// #     Ok(())
78    /// # }
79    /// ```
80    #[must_use]
81    pub fn new_without_plugins() -> Self {
82        Self {
83            swarm: SwarmBuilder::new_without_plugins(),
84        }
85    }
86
87    /// Set the function that's called every time a bot receives an
88    /// [`Event`](crate::Client). This is the way to handle normal per-bot
89    /// events.
90    ///
91    /// Currently, you can have up to one client handler.
92    ///
93    /// Note that if you're creating clients directly from the ECS using
94    /// [`StartJoinServerEvent`] and the client wasn't already in the ECS, then
95    /// the handler function won't be called for that client. This shouldn't be
96    /// a concern for most bots, though.
97    ///
98    /// ```
99    /// # use azalea::prelude::*;
100    /// # let client_builder = azalea::ClientBuilder::new();
101    /// client_builder.set_handler(handle);
102    ///
103    /// # #[derive(Clone, Component, Default)]
104    /// # pub struct State;
105    /// async fn handle(mut bot: Client, event: Event, state: State) -> anyhow::Result<()> {
106    ///     Ok(())
107    /// }
108    /// ```
109    ///
110    /// [`StartJoinServerEvent`]: azalea_client::join::StartJoinServerEvent
111    #[must_use]
112    pub fn set_handler<S, Fut, R>(self, handler: HandleFn<S, Fut>) -> ClientBuilder<S, R>
113    where
114        S: Default + Send + Sync + Clone + Component + 'static,
115        Fut: Future<Output = R> + Send + 'static,
116        R: Send + 'static,
117    {
118        ClientBuilder {
119            swarm: self.swarm.set_handler(handler),
120        }
121    }
122}
123impl<S, R> ClientBuilder<S, R>
124where
125    S: Default + Send + Sync + Clone + Component + 'static,
126    R: Send + 'static,
127{
128    /// Set the client state instead of initializing defaults.
129    #[must_use]
130    pub fn set_state(mut self, state: S) -> Self {
131        self.swarm.states = vec![state];
132        self
133    }
134    /// Add a group of plugins to the client.
135    ///
136    /// See [`Self::new_without_plugins`] to learn how to disable default
137    /// plugins.
138    #[must_use]
139    pub fn add_plugins<M>(mut self, plugins: impl Plugins<M>) -> Self {
140        self.swarm = self.swarm.add_plugins(plugins);
141        self
142    }
143
144    /// Configures the auto-reconnection behavior for our bot.
145    ///
146    /// If this is `Some`, then it'll set the default reconnection delay for our
147    /// bot (how long it'll wait after being kicked before it tries
148    /// rejoining). if it's `None`, then auto-reconnecting will be disabled.
149    ///
150    /// If this function isn't called, then our client will reconnect after
151    /// [`DEFAULT_RECONNECT_DELAY`].
152    ///
153    /// [`DEFAULT_RECONNECT_DELAY`]: azalea_client::auto_reconnect::DEFAULT_RECONNECT_DELAY
154    #[must_use]
155    pub fn reconnect_after(mut self, delay: impl Into<Option<Duration>>) -> Self {
156        self.swarm.reconnect_after = delay.into();
157        self
158    }
159
160    /// Build this `ClientBuilder` into an actual [`Client`](crate::Client) and
161    /// join the given server.
162    ///
163    /// If the client can't join, it'll keep retrying forever until it can.
164    ///
165    /// The `address` argument can be a `&str`, [`ServerAddr`],
166    /// [`ResolvedAddr`], or anything else that implements [`ResolvableAddr`].
167    ///
168    /// # Errors
169    ///
170    /// This will error if the given address is invalid or couldn't be resolved
171    /// to a Minecraft server.
172    ///
173    /// [`ServerAddr`]: azalea_protocol::address::ServerAddr
174    /// [`ResolvedAddr`]: azalea_protocol::address::ResolvedAddr
175    pub async fn start(mut self, account: Account, address: impl ResolvableAddr) -> AppExit {
176        self.swarm.accounts = vec![(account, JoinOpts::default())];
177        if self.swarm.states.is_empty() {
178            self.swarm.states = vec![S::default()];
179        }
180        self.swarm.start(address).await
181    }
182
183    /// Do the same as [`Self::start`], but allow passing in custom join
184    /// options.
185    pub async fn start_with_opts(
186        mut self,
187        account: Account,
188        address: impl ResolvableAddr,
189        opts: JoinOpts,
190    ) -> AppExit {
191        self.swarm.accounts = vec![(account, opts.clone())];
192        if self.swarm.states.is_empty() {
193            self.swarm.states = vec![S::default()];
194        }
195        self.swarm.start_with_opts(address, opts).await
196    }
197}
198impl Default for ClientBuilder<NoState, ()> {
199    fn default() -> Self {
200        Self::new()
201    }
202}