From 69b3ea7afb2721f26afa1ea55716e986a076b263 Mon Sep 17 00:00:00 2001 From: Michael Mikovsky <77305074+Astatin3@users.noreply.github.com> Date: Wed, 21 May 2025 12:10:42 -0600 Subject: [PATCH] Add stuff --- Cargo.toml | 7 +- src/bot_task/chat_task.rs | 38 ++++ src/bot_task/combat.rs | 26 +++ src/bot_task/debug.rs | 291 +++++++++++++++++++++++++++++++ src/bot_task/goto_block.rs | 85 +++++++++ src/bot_task/mod.rs | 8 + src/bot_task/movement.rs | 208 ++++++++++++++++++++++ src/command_controler.rs | 27 +++ src/killaura.rs | 48 +++++ src/main.rs | 348 ++++++++++++------------------------- src/swarm.rs | 260 +++++++++++++++++++++++++++ 11 files changed, 1107 insertions(+), 239 deletions(-) create mode 100644 src/bot_task/chat_task.rs create mode 100644 src/bot_task/combat.rs create mode 100644 src/bot_task/debug.rs create mode 100644 src/bot_task/goto_block.rs create mode 100644 src/bot_task/mod.rs create mode 100644 src/bot_task/movement.rs create mode 100644 src/command_controler.rs create mode 100644 src/killaura.rs create mode 100644 src/swarm.rs diff --git a/Cargo.toml b/Cargo.toml index 328a52b..6b82832 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,10 @@ edition = "2024" [dependencies] anyhow = "1.0.98" -azalea = "0.12.0" +azalea = { git = "https://github.com/azalea-rs/azalea", version = "0.12.0" } +azalea-world = "0.12.0" +bevy_ecs = "0.16.0" lazy_static = "1.5.0" -parking_lot = "0.12.3" +parking_lot = { version = "0.12.3", features = ["deadlock_detection"] } tokio = "1.45.0" +uuid = "1.16.0" diff --git a/src/bot_task/chat_task.rs b/src/bot_task/chat_task.rs new file mode 100644 index 0000000..3b18751 --- /dev/null +++ b/src/bot_task/chat_task.rs @@ -0,0 +1,38 @@ +use std::time::{Duration, Instant}; + +use azalea::{Client, Event}; + +use crate::command_controler::BotTask; + +pub struct Chat { + last_update: Instant, + messages: Vec, + index: usize, +} + +static DELAY: Duration = Duration::from_secs(1); + +impl Chat { + pub fn init(args: Vec) -> Self { + Self { + last_update: Instant::now() - DELAY, + messages: args, + index: 0, + } + } +} + +impl BotTask for Chat { + fn on_event(&mut self, bot: &Client, _event: &Event) { + if self.last_update.elapsed() >= DELAY { + bot.chat(self.messages[self.index].as_str()); + self.index += 1; + self.last_update = Instant::now(); + } + } + + fn end(&self) -> bool { + self.index == self.messages.len() + // Clean up + } +} diff --git a/src/bot_task/combat.rs b/src/bot_task/combat.rs new file mode 100644 index 0000000..4b66d8a --- /dev/null +++ b/src/bot_task/combat.rs @@ -0,0 +1,26 @@ +use azalea::brigadier::prelude::*; +use parking_lot::Mutex; + +use crate::{CommandControler::Ctx, CommandSource}; + +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/src/bot_task/debug.rs b/src/bot_task/debug.rs new file mode 100644 index 0000000..fb7ea48 --- /dev/null +++ b/src/bot_task/debug.rs @@ -0,0 +1,291 @@ +//! Commands for debugging and getting the current state of the bot. + +use std::{env, fs::File, io::Write, thread, time::Duration}; + +use azalea::{ + BlockPos, + brigadier::prelude::*, + chunks::ReceiveChunkEvent, + entity::{LookDirection, Position}, + interact::HitResultComponent, + packet::game, + pathfinder::{ExecutingPath, Pathfinder}, + world::{InstanceContainer, MinecraftEntityId}, +}; +use bevy_ecs::event::Events; +use parking_lot::Mutex; + +use crate::{CommandControler::Ctx, CommandSource}; + +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("disconnect").executes(|ctx: &Ctx| { + let source = ctx.source.lock(); + source.bot.disconnect(); + 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.is_miss() { + source.reply("I'm not looking at anything"); + return 1; + } + + let block_pos = &hit_result + .as_block_hit_result_if_not_miss() + .unwrap() + .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 + })); + + commands.register(literal("getblock").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!("getblock xyz {x} {y} {z}"); + let block_pos = BlockPos::new(x, y, z); + let block = source.bot.world().read().get_block_state(&block_pos); + source.reply(&format!("Block at {block_pos} is {block:?}")); + 1 + })), + ))); + commands.register(literal("getfluid").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!("getfluid xyz {x} {y} {z}"); + let block_pos = BlockPos::new(x, y, z); + let block = source.bot.world().read().get_fluid_state(&block_pos); + source.reply(&format!("Fluid at {block_pos} is {block:?}")); + 1 + })), + ))); + + commands.register(literal("pathfinderstate").executes(|ctx: &Ctx| { + let source = ctx.source.lock(); + let pathfinder = source.bot.get_component::(); + let Some(pathfinder) = pathfinder else { + source.reply("I don't have the Pathfinder ocmponent"); + return 1; + }; + source.reply(&format!( + "pathfinder.is_calculating: {}", + pathfinder.is_calculating + )); + + let executing_path = source.bot.get_component::(); + let Some(executing_path) = executing_path else { + source.reply("I'm not executing a path"); + return 1; + }; + source.reply(&format!( + "is_path_partial: {}, path.len: {}, queued_path.len: {}", + executing_path.is_path_partial, + executing_path.path.len(), + if let Some(queued) = executing_path.queued_path { + queued.len().to_string() + } else { + "n/a".to_string() + }, + )); + 1 + })); + + commands.register(literal("debugecsleak").executes(|ctx: &Ctx| { + let source = ctx.source.lock(); + + source.reply("Ok!"); + + + + source.bot.disconnect(); + + let ecs = source.bot.ecs.clone(); + thread::spawn(move || { + thread::sleep(Duration::from_secs(1)); + // dump the ecs + + let ecs = ecs.lock(); + + + + let report_path = env::temp_dir().join("azalea-ecs-leak-report.txt"); + let mut report = File::create(&report_path).unwrap(); + + for entity in ecs.iter_entities() { + writeln!(report, "Entity: {}", entity.id()).unwrap(); + let archetype = entity.archetype(); + let component_count = archetype.component_count(); + + let component_names = archetype + .components() + .map(|c| ecs.components().get_info(c).unwrap().name()) + .collect::>(); + writeln!( + report, + "- {component_count} components: {}", + component_names.join(", ") + ) + .unwrap(); + } + + writeln!(report).unwrap(); + + + for (info, _) in ecs.iter_resources() { + let name = info.name(); + writeln!(report, "Resource: {name}").unwrap(); + // writeln!(report, "- Size: {} bytes", + // info.layout().size()).unwrap(); + + match name { + "azalea_world::container::InstanceContainer" => { + let instance_container = ecs.resource::(); + + for (instance_name, instance) in &instance_container.instances { + writeln!(report, "- Name: {}", instance_name).unwrap(); + writeln!(report, "- Reference count: {}", instance.strong_count()) + .unwrap(); + if let Some(instance) = instance.upgrade() { + let instance = instance.read(); + let strong_chunks = instance + .chunks + .map + .iter() + .filter(|(_, v)| v.strong_count() > 0) + .count(); + writeln!( + report, + "- Chunks: {} strongly referenced, {} in map", + strong_chunks, + instance.chunks.map.len() + ) + .unwrap(); + writeln!( + report, + "- Entities: {}", + instance.entities_by_chunk.len() + ) + .unwrap(); + } + } + } + "bevy_ecs::event::collections::Events" => { + let events = ecs.resource::>(); + writeln!(report, "- Event count: {}", events.len()).unwrap(); + } + "bevy_ecs::event::collections::Events" => { + let events = ecs.resource::>(); + writeln!(report, "- Event count: {}", events.len()).unwrap(); + } + + _ => {} + } + } + + println!("\x1b[1mWrote report to {}\x1b[m", report_path.display()); + }); + + 1 + })); + + commands.register(literal("exit").executes(|ctx: &Ctx| { + let source = ctx.source.lock(); + source.reply("bye!"); + + source.bot.disconnect(); + + thread::spawn(move || { + thread::sleep(Duration::from_secs(1)); + + std::process::exit(0); + }); + + 1 + })); +} diff --git a/src/bot_task/goto_block.rs b/src/bot_task/goto_block.rs new file mode 100644 index 0000000..e4f5a0b --- /dev/null +++ b/src/bot_task/goto_block.rs @@ -0,0 +1,85 @@ +use azalea::{ + BlockPos, Client, Event, + pathfinder::{Pathfinder, goals}, + prelude::PathfinderClientExt, +}; + +use crate::command_controler::BotTask; + +pub struct GotoBlock { + x: i32, + y: Option, + z: i32, + started: bool, + finished: bool, +} + +impl GotoBlock { + pub fn parse(args: Vec) -> Option { + if args.len() == 3 { + let x = args[0].parse().ok()?; + let y = args[1].parse().ok()?; + let z = args[2].parse().ok()?; + Some(Self { + x, + y: Some(y), + z, + started: false, + finished: false, + }) + } else if args.len() == 2 { + let x = args[0].parse().ok()?; + let z = args[1].parse().ok()?; + Some(Self { + x, + y: None, + z, + started: false, + finished: false, + }) + } else { + return None; + } + } +} + +impl BotTask for GotoBlock { + fn on_event(&mut self, bot: &Client, event: &Event) { + if !self.started { + self.started = true; + bot.chat( + format!("Going to ({}, {}, {})", self.x, self.y.unwrap_or(0), self.z).as_str(), + ); + if let Some(y) = self.y { + bot.start_goto(goals::BlockPosGoal(BlockPos { + x: self.x, + y, + z: self.z, + })); + } else { + bot.start_goto(goals::XZGoal { + x: self.x, + z: self.z, + }); + } + } else { + match event { + Event::Tick => { + self.finished = { + let pos = bot.position().to_block_pos_floor(); + + pos.x == self.x + && pos.z == self.z + && (self.y.is_none() || pos.y == self.y.unwrap()) + } + } + _ => {} + } + } + } + + fn end(&self) -> bool { + // bot.chat("Arrived!") + self.finished + } +} diff --git a/src/bot_task/mod.rs b/src/bot_task/mod.rs new file mode 100644 index 0000000..299e977 --- /dev/null +++ b/src/bot_task/mod.rs @@ -0,0 +1,8 @@ +// pub mod combat; +// pub mod debug; +// pub mod movement; +pub mod chat_task; +pub mod goto_block; + +pub use chat_task::Chat; +pub use goto_block::GotoBlock; diff --git a/src/bot_task/movement.rs b/src/bot_task/movement.rs new file mode 100644 index 0000000..3f211f3 --- /dev/null +++ b/src/bot_task/movement.rs @@ -0,0 +1,208 @@ +use std::time::Duration; + +use azalea::{ + BlockPos, SprintDirection, WalkDirection, + brigadier::prelude::*, + entity::{EyeHeight, Position}, + pathfinder::goals::{BlockPosGoal, RadiusGoal, XZGoal}, + prelude::*, +}; +use parking_lot::Mutex; + +use crate::BotTask; +use crate::{CommandControler::Ctx, CommandSource}; + +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(literal("radius").then(argument("radius", float()).then( + argument("x", integer()).then(argument("y", integer()).then( + argument("z", integer()).executes(|ctx: &Ctx| { + let source = ctx.source.lock(); + let radius = get_float(ctx, "radius").unwrap(); + let x = get_integer(ctx, "x").unwrap(); + let y = get_integer(ctx, "y").unwrap(); + let z = get_integer(ctx, "z").unwrap(); + println!("goto radius {radius}, position: {x} {y} {z}"); + source.reply("ok"); + source.bot.goto(RadiusGoal { + pos: BlockPos::new(x, y, z).center(), + radius, + }); + 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 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 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 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 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 source = ctx.source.lock(); + source.bot.set_direction(180., 0.); + source.reply("ok"); + 1 + })); + commands.register(literal("south").executes(|ctx: &Ctx| { + let source = ctx.source.lock(); + source.bot.set_direction(0., 0.); + source.reply("ok"); + 1 + })); + commands.register(literal("east").executes(|ctx: &Ctx| { + let source = ctx.source.lock(); + source.bot.set_direction(-90., 0.); + source.reply("ok"); + 1 + })); + commands.register(literal("west").executes(|ctx: &Ctx| { + let source = ctx.source.lock(); + source.bot.set_direction(90., 0.); + source.reply("ok"); + 1 + })); + commands.register( + literal("jump") + .executes(|ctx: &Ctx| { + let 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 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/src/command_controler.rs b/src/command_controler.rs new file mode 100644 index 0000000..f295bdf --- /dev/null +++ b/src/command_controler.rs @@ -0,0 +1,27 @@ +use azalea::{Client, Event}; + +use crate::bot_task::*; + +pub trait BotTask: Send { + fn on_event(&mut self, bot: &Client, event: &Event); + fn end(&self) -> bool; +} + +pub fn parse_command(command: &str) -> Option> { + let args = command + .split_whitespace() + .map(|s| s.to_string()) + .collect::>(); + + match args[0].as_str() { + "!chat" => Some(Box::new(Chat::init(args[1..].to_vec()))), + "!goto" => { + if let Some(task) = GotoBlock::parse(args[1..].to_vec()) { + Some(Box::new(task)) + } else { + return None; + } + } + _ => None, + } +} diff --git a/src/killaura.rs b/src/killaura.rs new file mode 100644 index 0000000..b47ac0d --- /dev/null +++ b/src/killaura.rs @@ -0,0 +1,48 @@ +use azalea::{ + ecs::prelude::*, + entity::{Dead, LocalEntity, Position, metadata::AbstractMonster}, + prelude::*, + world::{InstanceName, MinecraftEntityId}, +}; + +use crate::State; + +pub fn tick(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/src/main.rs b/src/main.rs index 479d811..10da539 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,257 +1,131 @@ -use std::{collections::HashSet, sync::Arc, thread::sleep, time::Duration}; +use std::sync::Arc; + +pub mod bot_task; +pub mod command_controler; use azalea::{ - BlockPos, Bot, GameProfileComponent, - blocks::{BlockState, properties::Type}, - ecs::{entity::Entity, query::With}, - entity::{Position, metadata::Player}, - pathfinder::{GotoEvent, Pathfinder, astar::PathfinderTimeout, goals, moves::default_move}, prelude::*, - registry::{Block, Item}, - world::find_blocks::FindBlocks, + swarm::{Swarm, SwarmBuilder, SwarmEvent}, }; - -use lazy_static::lazy_static; +use command_controler::BotTask; use parking_lot::Mutex; -// use parking_lot::Mutex; + +pub static BOT_COUNT: usize = 3; +pub static BOT_PREFIX: &'static str = "bot"; #[tokio::main] -async fn main() { - let account = Account::offline("bot"); - // or Account::microsoft("example@example.com").await.unwrap(); - - ClientBuilder::new() +async fn main() -> Result<(), Box> { + SwarmBuilder::new() + .add_accounts( + (0..BOT_COUNT) + .map(|i| Account::offline(format!("{}{}", BOT_PREFIX, i).as_str())) + .collect(), + ) .set_handler(handle) - .start(account, "localhost") - .await - .unwrap(); + .set_swarm_handler(handle_swarm) + .start("localhost") + .await? + + // ClientBuilder::new() + // .set_handler(handle) + // .set_state(BotState { + // commands: Arc::new(Mutex::new(Vec::new())), + // }) + // .start(Account::offline("bot"), "localhost") + // .await?; } -#[derive(Clone)] -pub enum BotAction { - Nothing, - Goto(Position), - PathfindMine(PathfindMineAction), -} - -#[derive(Clone)] -pub enum PathfindMineAction { - Start, - Goto(HashSet, BlockPos), - Mine(HashSet, BlockPos), -} - -impl Default for BotAction { - fn default() -> Self { - Self::Nothing - } -} - -pub static MINE_DISTANCE: i32 = 7; - -lazy_static! { - pub static ref MINE_BLOCKS: HashSet = { - let mut b = HashSet::new(); - - b.insert(Block::OakLog.into()); - - b - }; -} - -#[derive(Default, Clone, Component)] -pub struct State { - pub action: Arc>, - // pub messages_received: Arc>, -} - -async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> { - match event { - Event::Chat(m) => { - let (sender, message) = m.split_sender_and_content(); - println!("<{:?}> {}", sender, message); - - if sender.is_some() && message.starts_with("!") { - match message.as_str() { - "!follow" => { - let uuid = m.sender_uuid().unwrap(); - let entity = bot.entity_by_uuid(uuid).unwrap(); - let position = bot.get_entity_component::(entity).unwrap(); - - bot.chat(format!("Following player {:?}", position).as_str()); - - bot.ecs.lock().send_event(GotoEvent { - entity: bot.entity, - goal: Arc::new(goals::BlockPosGoal(position.to_block_pos_ceil())), - successors_fn: default_move, - allow_mining: true, - min_timeout: PathfinderTimeout::Time(Duration::from_secs(2)), - max_timeout: PathfinderTimeout::Time(Duration::from_secs(10)), - }); - } - "!mine" => { - *state.action.lock() = BotAction::PathfindMine(PathfindMineAction::Start); - std::mem::drop(state.action); - - // blocks. - // - } - "!stop" => { - bot.stop_pathfinding(); - *state.action.lock() = BotAction::Nothing; - std::mem::drop(state.action); - } - _ => { - bot.chat(format!("Invalid command: {}", message).as_str()); - } - } - } - - // *state.messages_received.lock() += 1; +async fn handle(bot: Client, event: Event, state: BotState) -> anyhow::Result<()> { + if state.task.lock().is_some() { + // Process commands + if state.task.lock().as_ref().unwrap().end() { + bot.chat( + format!( + "Finished {}", + print_type_of(&state.task.lock().as_ref().unwrap()) + ) + .as_str(), + ); + *state.task.lock() = None; + } else { + state.task.lock().as_mut().unwrap().on_event(&bot, &event); } - Event::Tick => { - let action = state.action.lock().clone(); - match action { - BotAction::PathfindMine(action) => match action { - PathfindMineAction::Start => { - let block = bot - .world() - .read() - .find_blocks( - bot.position(), - &azalea::blocks::BlockStates { - set: MINE_BLOCKS.clone(), - }, - ) - .next() - .unwrap(); - - bot.ecs.lock().send_event(GotoEvent { - entity: bot.entity, - goal: Arc::new(goals::RadiusGoal { - pos: block.center(), - radius: MINE_DISTANCE as f32, - }), - successors_fn: default_move, - allow_mining: true, - min_timeout: PathfinderTimeout::Time(Duration::from_secs(2)), - max_timeout: PathfinderTimeout::Time(Duration::from_secs(10)), - }); - std::mem::drop(bot.ecs); - - // println!("Starting goto start..."); - - *state.action.lock() = BotAction::PathfindMine(PathfindMineAction::Goto( - MINE_BLOCKS.clone(), - block, - )); - std::mem::drop(state.action); - // println!("Finished lock"); - } - PathfindMineAction::Goto(blocks, block) => { - if is_goto_target_reached(&bot) { - bot.start_mining(block); - - // println!("Starting mine..."); - - *state.action.lock() = BotAction::PathfindMine( - PathfindMineAction::Mine(MINE_BLOCKS.clone(), block.clone()), - ); - std::mem::drop(state.action); - // println!("Droped mine..."); - } - } - PathfindMineAction::Mine(blocks, block) => { - println!("Stall 1"); - if let Some(blockstate) = bot.world().read().get_block_state(&block) { - println!("Stall 2"); - if blockstate.is_air() { - println!("Stall 3"); - if let Some(block) = bot - .world() - .read() - .find_blocks( - bot.position(), - &azalea::blocks::BlockStates { - set: MINE_BLOCKS.clone(), - }, - ) - .next() - { - println!("Stall 4"); - // { - // println!("Stall 5"); - // bot.ecs.lock() - // } - // .send_event(GotoEvent { - // entity: { - // println!("Stall 6"); - // bot.entity - // }, - // goal: Arc::new(goals::RadiusGoal { - // pos: block.center(), - // radius: MINE_DISTANCE as f32, - // }), - // successors_fn: default_move, - // allow_mining: true, - // min_timeout: PathfinderTimeout::Time(Duration::from_secs( - // 2, - // )), - // max_timeout: PathfinderTimeout::Time(Duration::from_secs( - // 10, - // )), - // }); - // println!("Stall 7"); - // - // std::mem::drop(bot.ecs); - - sleep(Duration::from_millis(100)); - - bot.goto(goals::RadiusGoal { - pos: block.center(), - radius: MINE_DISTANCE as f32, - }); - - // bot.is_go().await; - - println!("Stall 82"); - - println!("Starting Goto..."); - *state.action.lock() = BotAction::PathfindMine( - PathfindMineAction::Goto(MINE_BLOCKS.clone(), block), - ); - std::mem::drop(state.action); - println!("Dropped Goto..."); - } else { - *state.action.lock() = BotAction::Nothing; - std::mem::drop(state.action); - } - } - } - } - }, - - _ => {} - } + } else { + let swarm_state = bot.resource::(); + if !swarm_state.tasks.lock().is_empty() { + *state.task.lock() = Some(swarm_state.tasks.lock().remove(0)); + bot.chat( + format!( + "Starting {}", + print_type_of(&state.task.lock().as_ref().unwrap()) + ) + .as_str(), + ); } - _ => {} } Ok(()) } -fn is_goto_target_reached(bot: &Client) -> bool { - bot.map_get_component::(|p| { - p.map(|p| p.goal.is_none() && !p.is_calculating) - .unwrap_or(true) - }) +#[derive(Default, Clone, Component)] +pub struct BotState { + pub task: Arc>>>, + // pub messages_received: Arc>, } -fn player_by_name(bot: &Client, name: String) -> Option { - bot.entity_by::, (&GameProfileComponent,)>( - |(profile,): &(&GameProfileComponent,)| { - // return sender.unwrap() == profile.name; - profile.name == name - }, - ) +#[derive(Resource, Default, Clone)] +struct SwarmState { + pub tasks: Arc>>>, +} + +async fn handle_swarm(swarm: Swarm, event: SwarmEvent, state: SwarmState) -> anyhow::Result<()> { + match &event { + SwarmEvent::Init => { + println!("Swarm initialized"); + } + SwarmEvent::Login => { + println!("All bots have logged in"); + } + SwarmEvent::Disconnect(account, join_opts) => { + println!("Bot {} disconnected", account.username); + swarm + .add_with_opts(account, BotState::default(), join_opts) + .await?; + } + SwarmEvent::Chat(msg) => { + let command = msg.content(); + println!("Chat message: {}", command); + + println!("{}", command); + if let Some(command) = command_controler::parse_command(command.as_str()) { + state.tasks.lock().push(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:?}")); + // } + // } + } + _ => {} + } + Ok(()) +} + +fn print_type_of(_: &T) -> String { + std::any::type_name::().to_string() } diff --git a/src/swarm.rs b/src/swarm.rs new file mode 100644 index 0000000..6733d79 --- /dev/null +++ b/src/swarm.rs @@ -0,0 +1,260 @@ +//! 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. + +#![feature(trivial_bounds)] + +mod commands; +pub mod killaura; + +use std::time::Duration; +use std::{env, process}; +use std::{sync::Arc, thread}; + +use azalea::ClientInformation; +use azalea::brigadier::command_dispatcher::CommandDispatcher; +use azalea::ecs::prelude::*; +use azalea::pathfinder::debug::PathfinderDebugParticles; +use azalea::prelude::*; +use azalea::swarm::prelude::*; +use commands::{CommandSource, register_commands}; +use parking_lot::Mutex; + +#[tokio::main] +async fn main() { + 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); + + 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 + .unwrap(); +} + +/// 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, PartialEq, Eq, Default)] +pub enum BotTask { + #[default] + None, +} + +#[derive(Component, Clone, 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(Resource, Default, Clone)] +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() + }) + .await; + if swarm.args.pathfinder_debug_particles { + bot.ecs + .lock() + .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 => {} + } + } + _ => {} + } + + 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(Debug, Clone, Default)] +pub struct Args { + pub owner_username: String, + pub accounts: Vec, + pub server: String, + pub pathfinder_debug_particles: bool, +} + +fn parse_args() -> Args { + let mut owner_username = "admin".to_string(); + let mut accounts = Vec::new(); + let mut server = "localhost".to_string(); + let mut pathfinder_debug_particles = 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; + } + _ => { + eprintln!("Unknown argument: {}", arg); + process::exit(1); + } + } + } + + if accounts.is_empty() { + accounts.push("azalea".to_string()); + } + + Args { + owner_username, + accounts, + server, + pathfinder_debug_particles, + } +}