testbot/
main.rs

1//! A relatively simple bot for demonstrating some of Azalea's capabilities.
2//!
3//! ## Usage
4//!
5//! - Modify the consts below if necessary.
6//! - Run `cargo r --example testbot -- [arguments]`. (see below)
7//! - Commands are prefixed with `!` in chat. You can send them either in public
8//!   chat or as a /msg.
9//! - Some commands to try are `!goto`, `!killaura true`, `!down`. Check the
10//!   `commands` directory to see all of them.
11//!
12//! ### Arguments
13//!
14//! - `--owner` or `-O`: The username of the player who owns the bot. The bot
15//!   will ignore commands from other players.
16//! - `--account` or `-A`: The username or email of the bot.
17//! - `--server` or `-S`: The address of the server to join.
18//! - `--pathfinder-debug-particles` or `-P`: Whether the bot should run
19//!   /particle a ton of times to show where it's pathfinding to. You should
20//!   only have this on if the bot has operator permissions, otherwise it'll
21//!   just spam the server console unnecessarily.
22
23mod commands;
24pub mod killaura;
25
26use std::{env, process, sync::Arc, thread, time::Duration};
27
28use azalea::{
29    ClientInformation, brigadier::command_dispatcher::CommandDispatcher, ecs::prelude::*,
30    pathfinder::debug::PathfinderDebugParticles, prelude::*, swarm::prelude::*,
31};
32use commands::{CommandSource, register_commands};
33use parking_lot::Mutex;
34
35#[tokio::main]
36async fn main() -> AppExit {
37    let args = parse_args();
38
39    thread::spawn(deadlock_detection_thread);
40
41    let join_address = args.server.clone();
42
43    let mut builder = SwarmBuilder::new()
44        .set_handler(handle)
45        .set_swarm_handler(swarm_handle);
46
47    for username_or_email in &args.accounts {
48        let account = if username_or_email.contains('@') {
49            Account::microsoft(username_or_email).await.unwrap()
50        } else {
51            Account::offline(username_or_email)
52        };
53
54        builder = builder.add_account_with_state(account, State::new());
55    }
56
57    let mut commands = CommandDispatcher::new();
58    register_commands(&mut commands);
59
60    builder
61        .join_delay(Duration::from_millis(100))
62        .set_swarm_state(SwarmState {
63            args,
64            commands: Arc::new(commands),
65        })
66        .start(join_address)
67        .await
68}
69
70/// Runs a loop that checks for deadlocks every 10 seconds.
71///
72/// Note that this requires the `deadlock_detection` parking_lot feature to be
73/// enabled, which is only enabled in azalea by default when running in debug
74/// mode.
75fn deadlock_detection_thread() {
76    loop {
77        thread::sleep(Duration::from_secs(10));
78        let deadlocks = parking_lot::deadlock::check_deadlock();
79        if deadlocks.is_empty() {
80            continue;
81        }
82
83        println!("{} deadlocks detected", deadlocks.len());
84        for (i, threads) in deadlocks.iter().enumerate() {
85            println!("Deadlock #{i}");
86            for t in threads {
87                println!("Thread Id {:#?}", t.thread_id());
88                println!("{:#?}", t.backtrace());
89            }
90        }
91    }
92}
93
94#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
95pub enum BotTask {
96    #[default]
97    None,
98}
99
100#[derive(Clone, Component, Default)]
101pub struct State {
102    pub killaura: bool,
103    pub task: Arc<Mutex<BotTask>>,
104}
105
106impl State {
107    fn new() -> Self {
108        Self {
109            killaura: true,
110            task: Arc::new(Mutex::new(BotTask::None)),
111        }
112    }
113}
114
115#[derive(Clone, Default, Resource)]
116struct SwarmState {
117    pub args: Args,
118    pub commands: Arc<CommandDispatcher<Mutex<CommandSource>>>,
119}
120
121async fn handle(bot: Client, event: azalea::Event, state: State) -> anyhow::Result<()> {
122    let swarm = bot.resource::<SwarmState>();
123
124    match event {
125        azalea::Event::Init => {
126            bot.set_client_information(ClientInformation {
127                view_distance: 32,
128                ..Default::default()
129            });
130            if swarm.args.pathfinder_debug_particles {
131                bot.ecs
132                    .lock()
133                    .entity_mut(bot.entity)
134                    .insert(PathfinderDebugParticles);
135            }
136        }
137        azalea::Event::Chat(chat) => {
138            let (Some(username), content) = chat.split_sender_and_content() else {
139                return Ok(());
140            };
141            if username != swarm.args.owner_username {
142                return Ok(());
143            }
144
145            println!("{:?}", chat.message());
146
147            let command = if chat.is_whisper() {
148                Some(content)
149            } else {
150                content.strip_prefix('!').map(|s| s.to_owned())
151            };
152            if let Some(command) = command {
153                match swarm.commands.execute(
154                    command,
155                    Mutex::new(CommandSource {
156                        bot: bot.clone(),
157                        chat: chat.clone(),
158                        state: state.clone(),
159                    }),
160                ) {
161                    Ok(_) => {}
162                    Err(err) => {
163                        eprintln!("{err:?}");
164                        let command_source = CommandSource {
165                            bot,
166                            chat: chat.clone(),
167                            state: state.clone(),
168                        };
169                        command_source.reply(format!("{err:?}"));
170                    }
171                }
172            }
173        }
174        azalea::Event::Tick => {
175            killaura::tick(bot.clone(), state.clone())?;
176
177            let task = *state.task.lock();
178            match task {
179                BotTask::None => {}
180            }
181        }
182        azalea::Event::Login => {
183            println!("Got login event")
184        }
185        _ => {}
186    }
187
188    Ok(())
189}
190async fn swarm_handle(_swarm: Swarm, event: SwarmEvent, _state: SwarmState) -> anyhow::Result<()> {
191    match &event {
192        SwarmEvent::Disconnect(account, _join_opts) => {
193            println!("bot got kicked! {}", account.username);
194        }
195        SwarmEvent::Chat(chat) => {
196            if chat.message().to_string() == "The particle was not visible for anybody" {
197                return Ok(());
198            }
199            println!("{}", chat.message().to_ansi());
200        }
201        _ => {}
202    }
203
204    Ok(())
205}
206
207#[derive(Clone, Debug, Default)]
208pub struct Args {
209    pub owner_username: String,
210    pub accounts: Vec<String>,
211    pub server: String,
212    pub pathfinder_debug_particles: bool,
213}
214
215fn parse_args() -> Args {
216    let mut owner_username = "admin".to_owned();
217    let mut accounts = Vec::new();
218    let mut server = "localhost".to_owned();
219    let mut pathfinder_debug_particles = false;
220
221    let mut args = env::args().skip(1);
222    while let Some(arg) = args.next() {
223        match arg.as_str() {
224            "--owner" | "-O" => {
225                owner_username = args.next().expect("Missing owner username");
226            }
227            "--account" | "-A" => {
228                for account in args.next().expect("Missing account").split(',') {
229                    accounts.push(account.to_string());
230                }
231            }
232            "--server" | "-S" => {
233                server = args.next().expect("Missing server address");
234            }
235            "--pathfinder-debug-particles" | "-P" => {
236                pathfinder_debug_particles = true;
237            }
238            _ => {
239                eprintln!("Unknown argument: {arg}");
240                process::exit(1);
241            }
242        }
243    }
244
245    if accounts.is_empty() {
246        accounts.push("azalea".to_owned());
247    }
248
249    Args {
250        owner_username,
251        accounts,
252        server,
253        pathfinder_debug_particles,
254    }
255}