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}