Add stuff

This commit is contained in:
Michael Mikovsky
2025-05-21 12:10:42 -06:00
parent 018f0c03d4
commit 69b3ea7afb
11 changed files with 1107 additions and 239 deletions
+5 -2
View File
@@ -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"
+38
View File
@@ -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<String>,
index: usize,
}
static DELAY: Duration = Duration::from_secs(1);
impl Chat {
pub fn init(args: Vec<String>) -> 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
}
}
+26
View File
@@ -0,0 +1,26 @@
use azalea::brigadier::prelude::*;
use parking_lot::Mutex;
use crate::{CommandControler::Ctx, CommandSource};
pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) {
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::<crate::State>().unwrap();
// state.killaura = enabled
}
source.reply(if enabled {
"Enabled killaura"
} else {
"Disabled killaura"
});
1
})),
);
}
+291
View File
@@ -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<Mutex<CommandSource>>) {
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::<Position>(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::<MinecraftEntityId>(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::<LookDirection>();
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::<HitResultComponent>();
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::<Pathfinder>();
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::<ExecutingPath>();
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::<Vec<_>>();
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::<InstanceContainer>();
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<azalea_client::packet::game::ReceivePacketEvent>" => {
let events = ecs.resource::<Events<game::ReceiveGamePacketEvent>>();
writeln!(report, "- Event count: {}", events.len()).unwrap();
}
"bevy_ecs::event::collections::Events<azalea_client::chunks::ReceiveChunkEvent>" => {
let events = ecs.resource::<Events<ReceiveChunkEvent>>();
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
}));
}
+85
View File
@@ -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<i32>,
z: i32,
started: bool,
finished: bool,
}
impl GotoBlock {
pub fn parse(args: Vec<String>) -> Option<Self> {
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
}
}
+8
View File
@@ -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;
+208
View File
@@ -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<Mutex<CommandSource>>) {
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::<Position>(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::<Position>(entity) else {
source.reply("I can't see you!");
return 0;
};
let eye_height = source
.bot
.get_entity_component::<EyeHeight>(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
}));
}
+27
View File
@@ -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<Box<dyn BotTask>> {
let args = command
.split_whitespace()
.map(|s| s.to_string())
.collect::<Vec<String>>();
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,
}
}
+48
View File
@@ -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::<InstanceName>();
{
let mut ecs = bot.ecs.lock();
let mut query = ecs
.query_filtered::<(&MinecraftEntityId, &Position, &InstanceName), (
With<AbstractMonster>,
Without<LocalEntity>,
Without<Dead>,
)>();
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(())
}
+108 -234
View File
@@ -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<dyn std::error::Error>> {
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<BlockState>, BlockPos),
Mine(HashSet<BlockState>, BlockPos),
}
impl Default for BotAction {
fn default() -> Self {
Self::Nothing
}
}
pub static MINE_DISTANCE: i32 = 7;
lazy_static! {
pub static ref MINE_BLOCKS: HashSet<BlockState> = {
let mut b = HashSet::new();
b.insert(Block::OakLog.into());
b
};
}
#[derive(Default, Clone, Component)]
pub struct State {
pub action: Arc<Mutex<BotAction>>,
// pub messages_received: Arc<Mutex<usize>>,
}
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::<Position>(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;
}
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(),
},
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())
)
.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()),
.as_str(),
);
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...");
*state.task.lock() = None;
} else {
*state.action.lock() = BotAction::Nothing;
std::mem::drop(state.action);
state.task.lock().as_mut().unwrap().on_event(&bot, &event);
}
} else {
let swarm_state = bot.resource::<SwarmState>();
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::<Pathfinder, _>(|p| {
p.map(|p| p.goal.is_none() && !p.is_calculating)
.unwrap_or(true)
})
#[derive(Default, Clone, Component)]
pub struct BotState {
pub task: Arc<Mutex<Option<Box<dyn BotTask>>>>,
// pub messages_received: Arc<Mutex<usize>>,
}
fn player_by_name(bot: &Client, name: String) -> Option<Entity> {
bot.entity_by::<With<Player>, (&GameProfileComponent,)>(
|(profile,): &(&GameProfileComponent,)| {
// return sender.unwrap() == profile.name;
profile.name == name
},
)
#[derive(Resource, Default, Clone)]
struct SwarmState {
pub tasks: Arc<Mutex<Vec<Box<dyn BotTask>>>>,
}
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>(_: &T) -> String {
std::any::type_name::<T>().to_string()
}
+260
View File
@@ -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<Mutex<BotTask>>,
}
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<CommandDispatcher<Mutex<CommandSource>>>,
}
async fn handle(bot: Client, event: azalea::Event, state: State) -> anyhow::Result<()> {
let swarm = bot.resource::<SwarmState>();
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<String>,
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,
}
}