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