From 0aa439d5caa8028b6d310de45258cbcef16ca2eb Mon Sep 17 00:00:00 2001 From: mat Date: Sun, 7 Jan 2024 21:50:38 -0600 Subject: rewrite testbot to use brigadier --- azalea/examples/testbot/commands.rs | 46 +++++++ azalea/examples/testbot/commands/combat.rs | 26 ++++ azalea/examples/testbot/commands/debug.rs | 105 ++++++++++++++ azalea/examples/testbot/commands/movement.rs | 191 ++++++++++++++++++++++++++ azalea/examples/testbot/killaura.rs | 48 +++++++ azalea/examples/testbot/main.rs | 198 +++++++++++++++++++++++++++ 6 files changed, 614 insertions(+) create mode 100644 azalea/examples/testbot/commands.rs create mode 100644 azalea/examples/testbot/commands/combat.rs create mode 100644 azalea/examples/testbot/commands/debug.rs create mode 100644 azalea/examples/testbot/commands/movement.rs create mode 100644 azalea/examples/testbot/killaura.rs create mode 100644 azalea/examples/testbot/main.rs (limited to 'azalea/examples/testbot') diff --git a/azalea/examples/testbot/commands.rs b/azalea/examples/testbot/commands.rs new file mode 100644 index 00000000..db606aa6 --- /dev/null +++ b/azalea/examples/testbot/commands.rs @@ -0,0 +1,46 @@ +pub mod combat; +pub mod debug; +pub mod movement; + +use azalea::brigadier::prelude::*; +use azalea::chat::ChatPacket; +use azalea::ecs::prelude::Entity; +use azalea::ecs::prelude::*; +use azalea::entity::metadata::Player; +use azalea::Client; +use azalea::GameProfileComponent; +use parking_lot::Mutex; + +use crate::State; + +pub type Ctx = CommandContext>; + +pub struct CommandSource { + pub bot: Client, + pub state: State, + pub chat: ChatPacket, +} + +impl CommandSource { + pub fn reply(&self, message: &str) { + if self.chat.is_whisper() { + self.bot + .chat(&format!("/w {} {}", self.chat.username().unwrap(), message)); + } else { + self.bot.chat(message); + } + } + + pub fn entity(&mut self) -> Option { + let username = self.chat.username()?; + self.bot.entity_by::, &GameProfileComponent>( + |profile: &&GameProfileComponent| profile.name == username, + ) + } +} + +pub fn register_commands(commands: &mut CommandDispatcher>) { + combat::register(commands); + debug::register(commands); + movement::register(commands); +} diff --git a/azalea/examples/testbot/commands/combat.rs b/azalea/examples/testbot/commands/combat.rs new file mode 100644 index 00000000..b440b3ac --- /dev/null +++ b/azalea/examples/testbot/commands/combat.rs @@ -0,0 +1,26 @@ +use azalea::brigadier::prelude::*; +use parking_lot::Mutex; + +use super::{CommandSource, Ctx}; + +pub fn register(commands: &mut CommandDispatcher>) { + commands.register( + literal("killaura").then(argument("enabled", bool()).executes(|ctx: &Ctx| { + let enabled = get_bool(ctx, "enabled").unwrap(); + let source = ctx.source.lock(); + let bot = source.bot.clone(); + { + let mut ecs = bot.ecs.lock(); + let mut entity = ecs.entity_mut(bot.entity); + let mut state = entity.get_mut::().unwrap(); + state.killaura = enabled + } + source.reply(if enabled { + "Enabled killaura" + } else { + "Disabled killaura" + }); + 1 + })), + ); +} diff --git a/azalea/examples/testbot/commands/debug.rs b/azalea/examples/testbot/commands/debug.rs new file mode 100644 index 00000000..ae0808cb --- /dev/null +++ b/azalea/examples/testbot/commands/debug.rs @@ -0,0 +1,105 @@ +//! Commands for debugging and getting the current state of the bot. + +use azalea::{ + brigadier::prelude::*, + entity::{LookDirection, Position}, + interact::HitResultComponent, + world::MinecraftEntityId, +}; +use parking_lot::Mutex; + +use super::{CommandSource, Ctx}; + +pub fn register(commands: &mut CommandDispatcher>) { + commands.register(literal("ping").executes(|ctx: &Ctx| { + let source = ctx.source.lock(); + source.reply("pong!"); + 1 + })); + + commands.register(literal("whereami").executes(|ctx: &Ctx| { + let mut source = ctx.source.lock(); + let Some(entity) = source.entity() else { + source.reply("You aren't in render distance!"); + return 0; + }; + let position = source.bot.entity_component::(entity); + source.reply(&format!( + "You are at {}, {}, {}", + position.x, position.y, position.z + )); + 1 + })); + + commands.register(literal("entityid").executes(|ctx: &Ctx| { + let mut source = ctx.source.lock(); + let Some(entity) = source.entity() else { + source.reply("You aren't in render distance!"); + return 0; + }; + let entity_id = source.bot.entity_component::(entity); + source.reply(&format!( + "Your Minecraft ID is {} and your ECS id is {entity:?}", + *entity_id + )); + 1 + })); + + let whereareyou = |ctx: &Ctx| { + let source = ctx.source.lock(); + let position = source.bot.position(); + source.reply(&format!( + "I'm at {}, {}, {}", + position.x, position.y, position.z + )); + 1 + }; + commands.register(literal("whereareyou").executes(whereareyou)); + commands.register(literal("pos").executes(whereareyou)); + + commands.register(literal("whoareyou").executes(|ctx: &Ctx| { + let source = ctx.source.lock(); + source.reply(&format!( + "I am {} ({})", + source.bot.username(), + source.bot.uuid() + )); + 1 + })); + + commands.register(literal("getdirection").executes(|ctx: &Ctx| { + let source = ctx.source.lock(); + let direction = source.bot.component::(); + source.reply(&format!( + "I'm looking at {}, {}", + direction.y_rot, direction.x_rot + )); + 1 + })); + + commands.register(literal("health").executes(|ctx: &Ctx| { + let source = ctx.source.lock(); + + let health = source.bot.health(); + source.reply(&format!("I have {health} health")); + 1 + })); + + commands.register(literal("lookingat").executes(|ctx: &Ctx| { + let source = ctx.source.lock(); + + let hit_result = *source.bot.component::(); + + if hit_result.miss { + source.reply("I'm not looking at anything"); + return 1; + } + + let block_pos = hit_result.block_pos; + let block = source.bot.world().read().get_block_state(&block_pos); + + source.reply(&format!("I'm looking at {block:?} at {block_pos:?}")); + + 1 + })); +} diff --git a/azalea/examples/testbot/commands/movement.rs b/azalea/examples/testbot/commands/movement.rs new file mode 100644 index 00000000..4957533f --- /dev/null +++ b/azalea/examples/testbot/commands/movement.rs @@ -0,0 +1,191 @@ +use std::time::Duration; + +use azalea::{ + brigadier::prelude::*, + entity::{EyeHeight, Position}, + pathfinder::goals::{BlockPosGoal, XZGoal}, + prelude::*, + BlockPos, SprintDirection, WalkDirection, +}; +use parking_lot::Mutex; + +use crate::BotTask; + +use super::{CommandSource, Ctx}; + +pub fn register(commands: &mut CommandDispatcher>) { + commands.register( + literal("goto") + .executes(|ctx: &Ctx| { + let mut source = ctx.source.lock(); + println!("got goto"); + // look for the sender + let Some(entity) = source.entity() else { + source.reply("I can't see you!"); + return 0; + }; + let Some(position) = source.bot.get_entity_component::(entity) else { + source.reply("I can't see you!"); + return 0; + }; + source.reply("ok"); + source.bot.goto(BlockPosGoal(BlockPos::from(position))); + 1 + }) + .then(literal("xz").then(argument("x", integer()).then( + argument("z", integer()).executes(|ctx: &Ctx| { + let source = ctx.source.lock(); + let x = get_integer(ctx, "x").unwrap(); + let z = get_integer(ctx, "z").unwrap(); + println!("goto xz {x} {z}"); + source.reply("ok"); + source.bot.goto(XZGoal { x, z }); + 1 + }), + ))) + .then(argument("x", integer()).then(argument("y", integer()).then( + argument("z", integer()).executes(|ctx: &Ctx| { + let source = ctx.source.lock(); + let x = get_integer(ctx, "x").unwrap(); + let y = get_integer(ctx, "y").unwrap(); + let z = get_integer(ctx, "z").unwrap(); + println!("goto xyz {x} {y} {z}"); + source.reply("ok"); + source.bot.goto(BlockPosGoal(BlockPos::new(x, y, z))); + 1 + }), + ))), + ); + + commands.register(literal("down").executes(|ctx: &Ctx| { + let source = ctx.source.clone(); + tokio::spawn(async move { + let mut bot = source.lock().bot.clone(); + let position = BlockPos::from(bot.position()); + source.lock().reply("mining..."); + bot.mine(position.down(1)).await; + source.lock().reply("done"); + }); + 1 + })); + + commands.register( + literal("look") + .executes(|ctx: &Ctx| { + // look for the sender + let mut source = ctx.source.lock(); + let Some(entity) = source.entity() else { + source.reply("I can't see you!"); + return 0; + }; + let Some(position) = source.bot.get_entity_component::(entity) else { + source.reply("I can't see you!"); + return 0; + }; + let eye_height = source + .bot + .get_entity_component::(entity) + .map(|h| *h) + .unwrap_or_default(); + source.bot.look_at(position.up(eye_height as f64)); + 1 + }) + .then(argument("x", integer()).then(argument("y", integer()).then( + argument("z", integer()).executes(|ctx: &Ctx| { + let pos = BlockPos::new( + get_integer(ctx, "x").unwrap(), + get_integer(ctx, "y").unwrap(), + get_integer(ctx, "z").unwrap(), + ); + println!("{:?}", pos); + let mut source = ctx.source.lock(); + source.bot.look_at(pos.center()); + 1 + }), + ))), + ); + + commands.register( + literal("walk").then(argument("seconds", float()).executes(|ctx: &Ctx| { + let mut seconds = get_float(ctx, "seconds").unwrap(); + let source = ctx.source.lock(); + let mut bot = source.bot.clone(); + + if seconds < 0. { + bot.walk(WalkDirection::Backward); + seconds = -seconds; + } else { + bot.walk(WalkDirection::Forward); + } + + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs_f32(seconds)).await; + bot.walk(WalkDirection::None); + }); + source.reply(&format!("ok, walking for {seconds} seconds")); + 1 + })), + ); + commands.register( + literal("sprint").then(argument("seconds", float()).executes(|ctx: &Ctx| { + let seconds = get_float(ctx, "seconds").unwrap(); + let source = ctx.source.lock(); + let mut bot = source.bot.clone(); + bot.sprint(SprintDirection::Forward); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs_f32(seconds)).await; + bot.walk(WalkDirection::None); + }); + source.reply(&format!("ok, spriting for {seconds} seconds")); + 1 + })), + ); + + commands.register(literal("north").executes(|ctx: &Ctx| { + let mut source = ctx.source.lock(); + source.bot.set_direction(180., 0.); + source.reply("ok"); + 1 + })); + commands.register(literal("south").executes(|ctx: &Ctx| { + let mut source = ctx.source.lock(); + source.bot.set_direction(0., 0.); + source.reply("ok"); + 1 + })); + commands.register(literal("east").executes(|ctx: &Ctx| { + let mut source = ctx.source.lock(); + source.bot.set_direction(-90., 0.); + source.reply("ok"); + 1 + })); + commands.register(literal("west").executes(|ctx: &Ctx| { + let mut source = ctx.source.lock(); + source.bot.set_direction(90., 0.); + source.reply("ok"); + 1 + })); + commands.register( + literal("jump") + .executes(|ctx: &Ctx| { + let mut source = ctx.source.lock(); + source.bot.jump(); + source.reply("ok"); + 1 + }) + .then(argument("enabled", bool()).executes(|ctx: &Ctx| { + let jumping = get_bool(ctx, "enabled").unwrap(); + let mut source = ctx.source.lock(); + source.bot.set_jumping(jumping); + 1 + })), + ); + + commands.register(literal("stop").executes(|ctx: &Ctx| { + let source = ctx.source.lock(); + source.bot.stop_pathfinding(); + source.reply("ok"); + *source.state.task.lock() = BotTask::None; + 1 + })); +} diff --git a/azalea/examples/testbot/killaura.rs b/azalea/examples/testbot/killaura.rs new file mode 100644 index 00000000..d86356fe --- /dev/null +++ b/azalea/examples/testbot/killaura.rs @@ -0,0 +1,48 @@ +use azalea::{ + ecs::prelude::*, + entity::{metadata::AbstractMonster, Dead, LocalEntity, Position}, + prelude::*, + world::{InstanceName, MinecraftEntityId}, +}; + +use crate::State; + +pub fn tick(mut bot: Client, state: State) -> anyhow::Result<()> { + if !state.killaura { + return Ok(()); + } + if bot.has_attack_cooldown() { + return Ok(()); + } + let mut nearest_entity = None; + let mut nearest_distance = f64::INFINITY; + let bot_position = bot.eye_position(); + let bot_instance_name = bot.component::(); + { + let mut ecs = bot.ecs.lock(); + let mut query = ecs + .query_filtered::<(&MinecraftEntityId, &Position, &InstanceName), ( + With, + Without, + Without, + )>(); + for (&entity_id, position, instance_name) in query.iter(&ecs) { + if instance_name != &bot_instance_name { + continue; + } + + let distance = bot_position.distance_to(position); + if distance < 4. && distance < nearest_distance { + nearest_entity = Some(entity_id); + nearest_distance = distance; + } + } + } + if let Some(nearest_entity) = nearest_entity { + println!("attacking {:?}", nearest_entity); + println!("distance {:?}", nearest_distance); + bot.attack(nearest_entity); + } + + Ok(()) +} diff --git a/azalea/examples/testbot/main.rs b/azalea/examples/testbot/main.rs new file mode 100644 index 00000000..9a3bd4f8 --- /dev/null +++ b/azalea/examples/testbot/main.rs @@ -0,0 +1,198 @@ +//! A relatively simple bot for demonstrating some of Azalea's capabilities. +//! +//! Usage: +//! - Modify the consts below if necessary. +//! - Run `cargo r --example testbot` +//! - 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`, `!down`. Check the +//! `commands` directory to see all of them. + +#![feature(async_closure)] +#![feature(trivial_bounds)] + +mod commands; +pub mod killaura; + +use azalea::pathfinder::PathfinderDebugParticles; +use azalea::{Account, ClientInformation}; + +use azalea::brigadier::command_dispatcher::CommandDispatcher; +use azalea::ecs::prelude::*; +use azalea::prelude::*; +use azalea::swarm::prelude::*; +use commands::{register_commands, CommandSource}; +use parking_lot::Mutex; +use std::sync::Arc; +use std::time::Duration; + +const USERNAME: &str = "azalea"; +const ADDRESS: &str = "localhost"; +/// 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. +const PATHFINDER_DEBUG_PARTICLES: bool = true; + +#[tokio::main] +async fn main() { + { + use parking_lot::deadlock; + use std::thread; + use std::time::Duration; + + // Create a background thread which checks for deadlocks every 10s + thread::spawn(move || loop { + thread::sleep(Duration::from_secs(10)); + let deadlocks = 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()); + } + } + }); + } + + let account = Account::offline(USERNAME); + + let mut commands = CommandDispatcher::new(); + register_commands(&mut commands); + let commands = Arc::new(commands); + + let builder = SwarmBuilder::new(); + builder + .set_handler(handle) + .set_swarm_handler(swarm_handle) + .add_account_with_state( + account, + State { + commands: commands.clone(), + ..Default::default() + }, + ) + .join_delay(Duration::from_millis(100)) + .start(ADDRESS) + .await + .unwrap(); +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum BotTask { + #[default] + None, +} + +#[derive(Component, Clone)] +pub struct State { + pub commands: Arc>>, + pub killaura: bool, + pub task: Arc>, +} + +impl Default for State { + fn default() -> Self { + Self { + commands: Arc::new(CommandDispatcher::new()), + killaura: true, + task: Arc::new(Mutex::new(BotTask::None)), + } + } +} + +#[derive(Resource, Default, Clone)] +struct SwarmState; + +async fn handle(bot: Client, event: azalea::Event, state: State) -> anyhow::Result<()> { + match event { + azalea::Event::Init => { + bot.set_client_information(ClientInformation { + view_distance: 32, + ..Default::default() + }) + .await?; + if PATHFINDER_DEBUG_PARTICLES { + bot.ecs + .lock() + .entity_mut(bot.entity) + .insert(PathfinderDebugParticles); + } + } + azalea::Event::Login => {} + azalea::Event::Chat(chat) => { + let (Some(username), content) = chat.split_sender_and_content() else { + return Ok(()); + }; + if username != "py5" { + 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 state.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().clone(); + match task { + BotTask::None => {} + } + } + _ => {} + } + + Ok(()) +} +async fn swarm_handle( + mut swarm: Swarm, + event: SwarmEvent, + _state: SwarmState, +) -> anyhow::Result<()> { + match &event { + SwarmEvent::Disconnect(account) => { + println!("bot got kicked! {}", account.username); + tokio::time::sleep(Duration::from_secs(5)).await; + swarm.add_and_retry_forever(account, State::default()).await; + } + SwarmEvent::Chat(chat) => { + if chat.message().to_string() == "The particle was not visible for anybody" { + return Ok(()); + } + println!("{}", chat.message().to_ansi()); + } + _ => {} + } + + Ok(()) +} -- cgit v1.2.3