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<CommandDispatcher<Mutex<CommandSource>>>,
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(_) => {}
173                    Err(err) => {
174                        eprintln!("{err:?}");
175                        let command_source = CommandSource {
176                            bot,
177                            chat: chat.clone(),
178                            state: state.clone(),
179                        };
180                        command_source.reply(format!("{err:?}"));
181                    }
182                }
183            }
184        }
185        azalea::Event::Tick => {
186            killaura::tick(bot.clone(), state.clone())?;
187
188            if bot.ticks_connected().is_multiple_of(5) {
189                if let Some(following) = &*state.following_entity.lock()
190                    && following.is_alive()
191                {
192                    let goal = RadiusGoal::new(following.position(), 3.);
193                    if bot.is_calculating_path() {
194                        // keep waiting
195                    } else if !goal.success(bot.position().into()) || bot.is_executing_path() {
196                        bot.start_goto_with_opts(
197                            goal,
198                            PathfinderOpts::new()
199                                .retry_on_no_path(false)
200                                .max_timeout(Duration::from_secs(1)),
201                        );
202                    } else {
203                        following.look_at();
204                    }
205                }
206            }
207        }
208        azalea::Event::Login => {
209            println!("Got login event")
210        }
211        _ => {}
212    }
213
214    Ok(())
215}
216async fn swarm_handle(_swarm: Swarm, event: SwarmEvent, _state: SwarmState) -> eyre::Result<()> {
217    match &event {
218        SwarmEvent::Disconnect(account, _join_opts) => {
219            println!("bot got kicked! {}", account.username());
220        }
221        SwarmEvent::Chat(chat) => {
222            if chat.message().to_string() == "The particle was not visible for anybody" {
223                return Ok(());
224            }
225            println!("{}", chat.message().to_ansi());
226        }
227        _ => {}
228    }
229
230    Ok(())
231}
232
233#[derive(Clone, Debug, Default)]
234pub struct Args {
235    pub owner_username: String,
236    pub accounts: Vec<String>,
237    pub server: String,
238    pub pathfinder_debug_particles: bool,
239    pub simulation_pathfinder: bool,
240}
241
242fn parse_args() -> Args {
243    let mut owner_username = "admin".to_owned();
244    let mut accounts = Vec::new();
245    let mut server = "localhost".to_owned();
246    let mut pathfinder_debug_particles = false;
247    let mut simulation_pathfinder = false;
248
249    let mut args = env::args().skip(1);
250    while let Some(arg) = args.next() {
251        match arg.as_str() {
252            "--owner" | "-O" => {
253                owner_username = args.next().expect("Missing owner username");
254            }
255            "--account" | "-A" => {
256                for account in args.next().expect("Missing account").split(',') {
257                    accounts.push(account.to_string());
258                }
259            }
260            "--server" | "-S" => {
261                server = args.next().expect("Missing server address");
262            }
263            "--pathfinder-debug-particles" | "-P" => {
264                pathfinder_debug_particles = true;
265            }
266            "--simulation-pathfinder" => {
267                simulation_pathfinder = true;
268            }
269            _ => {
270                eprintln!("Unknown argument: {arg}");
271                process::exit(1);
272            }
273        }
274    }
275
276    if accounts.is_empty() {
277        accounts.push("azalea".to_owned());
278    }
279
280    Args {
281        owner_username,
282        accounts,
283        server,
284        pathfinder_debug_particles,
285        simulation_pathfinder,
286    }
287}