Skip to main content

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