azalea 0.16.0+mc26.1

A framework for creating Minecraft bots.
Documentation
//! 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;
pub mod mspt;

use std::{env, process, sync::Arc, thread, time::Duration};

use azalea::{
    ClientInformation, EntityRef,
    brigadier::command_dispatcher::CommandDispatcher,
    ecs::prelude::*,
    pathfinder::{
        PathfinderOpts,
        debug::PathfinderDebugParticles,
        execute::simulation::SimulationPathfinderExecutionPlugin,
        goals::{Goal, RadiusGoal},
    },
    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: args.into(),
            commands: commands.into(),
        })
        // .add_plugins(mspt::MsptPlugin)
        .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, Component, Default)]
pub struct State {
    pub killaura: bool,
    pub following_entity: Arc<Mutex<Option<EntityRef>>>,
}

impl State {
    fn new() -> Self {
        Self {
            killaura: false,
            following_entity: Default::default(),
        }
    }
}

#[derive(Clone, Default, Resource)]
struct SwarmState {
    pub args: Arc<Args>,
    pub commands: Arc<CommandDispatcher<Mutex<CommandSource>>>,
}

async fn handle(bot: Client, event: azalea::Event, state: State) -> eyre::Result<()> {
    let swarm = bot.resource::<SwarmState>();

    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())?;

            if bot.ticks_connected().is_multiple_of(5) {
                if let Some(following) = &*state.following_entity.lock()
                    && following.is_alive()
                {
                    let goal = RadiusGoal::new(following.position(), 3.);
                    if bot.is_calculating_path() {
                        // keep waiting
                    } else if !goal.success(bot.position().into()) || bot.is_executing_path() {
                        bot.start_goto_with_opts(
                            goal,
                            PathfinderOpts::new()
                                .retry_on_no_path(false)
                                .max_timeout(Duration::from_secs(1)),
                        );
                    } else {
                        following.look_at();
                    }
                }
            }
        }
        azalea::Event::Login => {
            println!("Got login event")
        }
        _ => {}
    }

    Ok(())
}
async fn swarm_handle(_swarm: Swarm, event: SwarmEvent, _state: SwarmState) -> eyre::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<String>,
    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,
    }
}