mirror of
https://github.com/Astatin3/rustbot.git
synced 2026-06-08 16:18:08 -06:00
Add stuff
This commit is contained in:
+5
-2
@@ -5,7 +5,10 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.98"
|
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"
|
lazy_static = "1.5.0"
|
||||||
parking_lot = "0.12.3"
|
parking_lot = { version = "0.12.3", features = ["deadlock_detection"] }
|
||||||
tokio = "1.45.0"
|
tokio = "1.45.0"
|
||||||
|
uuid = "1.16.0"
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
+111
-237
@@ -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::{
|
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::*,
|
prelude::*,
|
||||||
registry::{Block, Item},
|
swarm::{Swarm, SwarmBuilder, SwarmEvent},
|
||||||
world::find_blocks::FindBlocks,
|
|
||||||
};
|
};
|
||||||
|
use command_controler::BotTask;
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
// use parking_lot::Mutex;
|
|
||||||
|
pub static BOT_COUNT: usize = 3;
|
||||||
|
pub static BOT_PREFIX: &'static str = "bot";
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let account = Account::offline("bot");
|
SwarmBuilder::new()
|
||||||
// or Account::microsoft("example@example.com").await.unwrap();
|
.add_accounts(
|
||||||
|
(0..BOT_COUNT)
|
||||||
ClientBuilder::new()
|
.map(|i| Account::offline(format!("{}{}", BOT_PREFIX, i).as_str()))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
.set_handler(handle)
|
.set_handler(handle)
|
||||||
.start(account, "localhost")
|
.set_swarm_handler(handle_swarm)
|
||||||
.await
|
.start("localhost")
|
||||||
.unwrap();
|
.await?
|
||||||
|
|
||||||
|
// ClientBuilder::new()
|
||||||
|
// .set_handler(handle)
|
||||||
|
// .set_state(BotState {
|
||||||
|
// commands: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
// })
|
||||||
|
// .start(Account::offline("bot"), "localhost")
|
||||||
|
// .await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
async fn handle(bot: Client, event: Event, state: BotState) -> anyhow::Result<()> {
|
||||||
pub enum BotAction {
|
if state.task.lock().is_some() {
|
||||||
Nothing,
|
// Process commands
|
||||||
Goto(Position),
|
if state.task.lock().as_ref().unwrap().end() {
|
||||||
PathfindMine(PathfindMineAction),
|
bot.chat(
|
||||||
}
|
format!(
|
||||||
|
"Finished {}",
|
||||||
#[derive(Clone)]
|
print_type_of(&state.task.lock().as_ref().unwrap())
|
||||||
pub enum PathfindMineAction {
|
)
|
||||||
Start,
|
.as_str(),
|
||||||
Goto(HashSet<BlockState>, BlockPos),
|
);
|
||||||
Mine(HashSet<BlockState>, BlockPos),
|
*state.task.lock() = None;
|
||||||
}
|
} else {
|
||||||
|
state.task.lock().as_mut().unwrap().on_event(&bot, &event);
|
||||||
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 => {
|
} else {
|
||||||
let action = state.action.lock().clone();
|
let swarm_state = bot.resource::<SwarmState>();
|
||||||
match action {
|
if !swarm_state.tasks.lock().is_empty() {
|
||||||
BotAction::PathfindMine(action) => match action {
|
*state.task.lock() = Some(swarm_state.tasks.lock().remove(0));
|
||||||
PathfindMineAction::Start => {
|
bot.chat(
|
||||||
let block = bot
|
format!(
|
||||||
.world()
|
"Starting {}",
|
||||||
.read()
|
print_type_of(&state.task.lock().as_ref().unwrap())
|
||||||
.find_blocks(
|
)
|
||||||
bot.position(),
|
.as_str(),
|
||||||
&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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_goto_target_reached(bot: &Client) -> bool {
|
#[derive(Default, Clone, Component)]
|
||||||
bot.map_get_component::<Pathfinder, _>(|p| {
|
pub struct BotState {
|
||||||
p.map(|p| p.goal.is_none() && !p.is_calculating)
|
pub task: Arc<Mutex<Option<Box<dyn BotTask>>>>,
|
||||||
.unwrap_or(true)
|
// pub messages_received: Arc<Mutex<usize>>,
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn player_by_name(bot: &Client, name: String) -> Option<Entity> {
|
#[derive(Resource, Default, Clone)]
|
||||||
bot.entity_by::<With<Player>, (&GameProfileComponent,)>(
|
struct SwarmState {
|
||||||
|(profile,): &(&GameProfileComponent,)| {
|
pub tasks: Arc<Mutex<Vec<Box<dyn BotTask>>>>,
|
||||||
// return sender.unwrap() == profile.name;
|
}
|
||||||
profile.name == name
|
|
||||||
},
|
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
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user