//! A relatively simple bot for demonstrating some of Azalea's capabilities. //! //! ## Usage //! //! - Modify the consts below if necessary. //! - Run `cargo r --example testbot -- [arguments]`. (see below) //! - Commands are prefixed with `!` in chat. You can send them either in public //! chat or as a /msg. //! - Some commands to try are `!goto`, `!killaura true`, `!down`. Check the //! `commands` directory to see all of them. //! //! ### Arguments //! //! - `--owner` or `-O`: The username of the player who owns the bot. The bot //! will ignore commands from other players. //! - `--account` or `-A`: The username or email of the bot. //! - `--server` or `-S`: The address of the server to join. //! - `--pathfinder-debug-particles` or `-P`: Whether the bot should run //! /particle a ton of times to show where it's pathfinding to. You should //! only have this on if the bot has operator permissions, otherwise it'll //! just spam the server console unnecessarily. //! - `--simulation-pathfinder`: Use the alternative simulation-based execution //! engine for the pathfinder. mod commands; pub mod killaura; use std::{env, process, sync::Arc, thread, time::Duration}; use azalea::{ ClientInformation, brigadier::command_dispatcher::CommandDispatcher, ecs::prelude::*, pathfinder::{ debug::PathfinderDebugParticles, execute::simulation::SimulationPathfinderExecutionPlugin, }, prelude::*, swarm::prelude::*, }; use commands::{CommandSource, register_commands}; use parking_lot::Mutex; #[tokio::main] async fn main() -> AppExit { let args = parse_args(); thread::spawn(deadlock_detection_thread); let join_address = args.server.clone(); let mut builder = SwarmBuilder::new() .set_handler(handle) .set_swarm_handler(swarm_handle); if args.simulation_pathfinder { builder = builder.add_plugins(SimulationPathfinderExecutionPlugin); } for username_or_email in &args.accounts { let account = if username_or_email.contains('@') { Account::microsoft(username_or_email).await.unwrap() } else { Account::offline(username_or_email) }; builder = builder.add_account_with_state(account, State::new()); } let mut commands = CommandDispatcher::new(); register_commands(&mut commands); builder .join_delay(Duration::from_millis(100)) .set_swarm_state(SwarmState { args, commands: Arc::new(commands), }) .start(join_address) .await } /// Runs a loop that checks for deadlocks every 10 seconds. /// /// Note that this requires the `deadlock_detection` parking_lot feature to be /// enabled, which is only enabled in azalea by default when running in debug /// mode. fn deadlock_detection_thread() { loop { thread::sleep(Duration::from_secs(10)); let deadlocks = parking_lot::deadlock::check_deadlock(); if deadlocks.is_empty() { continue; } println!("{} deadlocks detected", deadlocks.len()); for (i, threads) in deadlocks.iter().enumerate() { println!("Deadlock #{i}"); for t in threads { println!("Thread Id {:#?}", t.thread_id()); println!("{:#?}", t.backtrace()); } } } } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum BotTask { #[default] None, } #[derive(Clone, Component, Default)] pub struct State { pub killaura: bool, pub task: Arc>, } impl State { fn new() -> Self { Self { killaura: true, task: Arc::new(Mutex::new(BotTask::None)), } } } #[derive(Clone, Default, Resource)] struct SwarmState { pub args: Args, pub commands: Arc>>, } async fn handle(bot: Client, event: azalea::Event, state: State) -> anyhow::Result<()> { let swarm = bot.resource::(); match event { azalea::Event::Init => { bot.set_client_information(ClientInformation { view_distance: 32, ..Default::default() }); if swarm.args.pathfinder_debug_particles { bot.ecs .write() .entity_mut(bot.entity) .insert(PathfinderDebugParticles); } } azalea::Event::Chat(chat) => { let (Some(username), content) = chat.split_sender_and_content() else { return Ok(()); }; if username != swarm.args.owner_username { return Ok(()); } println!("{:?}", chat.message()); let command = if chat.is_whisper() { Some(content) } else { content.strip_prefix('!').map(|s| s.to_owned()) }; if let Some(command) = command { match swarm.commands.execute( command, Mutex::new(CommandSource { bot: bot.clone(), chat: chat.clone(), state: state.clone(), }), ) { Ok(_) => {} Err(err) => { eprintln!("{err:?}"); let command_source = CommandSource { bot, chat: chat.clone(), state: state.clone(), }; command_source.reply(format!("{err:?}")); } } } } azalea::Event::Tick => { killaura::tick(bot.clone(), state.clone())?; let task = *state.task.lock(); match task { BotTask::None => {} } } azalea::Event::Login => { println!("Got login event") } _ => {} } Ok(()) } async fn swarm_handle(_swarm: Swarm, event: SwarmEvent, _state: SwarmState) -> anyhow::Result<()> { match &event { SwarmEvent::Disconnect(account, _join_opts) => { println!("bot got kicked! {}", account.username()); } SwarmEvent::Chat(chat) => { if chat.message().to_string() == "The particle was not visible for anybody" { return Ok(()); } println!("{}", chat.message().to_ansi()); } _ => {} } Ok(()) } #[derive(Clone, Debug, Default)] pub struct Args { pub owner_username: String, pub accounts: Vec, pub server: String, pub pathfinder_debug_particles: bool, pub simulation_pathfinder: bool, } fn parse_args() -> Args { let mut owner_username = "admin".to_owned(); let mut accounts = Vec::new(); let mut server = "localhost".to_owned(); let mut pathfinder_debug_particles = false; let mut simulation_pathfinder = false; let mut args = env::args().skip(1); while let Some(arg) = args.next() { match arg.as_str() { "--owner" | "-O" => { owner_username = args.next().expect("Missing owner username"); } "--account" | "-A" => { for account in args.next().expect("Missing account").split(',') { accounts.push(account.to_string()); } } "--server" | "-S" => { server = args.next().expect("Missing server address"); } "--pathfinder-debug-particles" | "-P" => { pathfinder_debug_particles = true; } "--simulation-pathfinder" => { simulation_pathfinder = true; } _ => { eprintln!("Unknown argument: {arg}"); process::exit(1); } } } if accounts.is_empty() { accounts.push("azalea".to_owned()); } Args { owner_username, accounts, server, pathfinder_debug_particles, simulation_pathfinder, } }