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