aboutsummaryrefslogtreecommitdiff
path: root/azalea
diff options
context:
space:
mode:
Diffstat (limited to 'azalea')
-rwxr-xr-xazalea/README.md2
-rw-r--r--azalea/examples/testbot.rs424
-rw-r--r--azalea/examples/testbot/commands.rs46
-rw-r--r--azalea/examples/testbot/commands/combat.rs26
-rw-r--r--azalea/examples/testbot/commands/debug.rs105
-rw-r--r--azalea/examples/testbot/commands/movement.rs191
-rw-r--r--azalea/examples/testbot/killaura.rs48
-rw-r--r--azalea/examples/testbot/main.rs198
8 files changed, 615 insertions, 425 deletions
diff --git a/azalea/README.md b/azalea/README.md
index 339c57a2..f4095191 100755
--- a/azalea/README.md
+++ b/azalea/README.md
@@ -53,7 +53,7 @@ async fn main() {
ClientBuilder::new()
.set_handler(handle)
- .start(account.clone(), "localhost")
+ .start(account, "localhost")
.await
.unwrap();
}
diff --git a/azalea/examples/testbot.rs b/azalea/examples/testbot.rs
deleted file mode 100644
index c9e64ef9..00000000
--- a/azalea/examples/testbot.rs
+++ /dev/null
@@ -1,424 +0,0 @@
-//! a bot for testing new azalea features
-
-use azalea::ecs::query::With;
-use azalea::entity::{metadata::Player, EyeHeight, Position};
-use azalea::interact::HitResultComponent;
-use azalea::inventory::ItemSlot;
-use azalea::pathfinder::goals::BlockPosGoal;
-use azalea::{prelude::*, swarm::prelude::*, BlockPos, GameProfileComponent, WalkDirection};
-use azalea::{Account, Client, Event};
-use azalea_client::{InstanceHolder, SprintDirection};
-use azalea_core::position::{ChunkBlockPos, ChunkPos, Vec3};
-use azalea_protocol::packets::game::ClientboundGamePacket;
-use azalea_world::heightmap::HeightmapKind;
-use azalea_world::{InstanceName, MinecraftEntityId};
-use std::time::Duration;
-
-#[derive(Default, Clone, Component)]
-struct State {}
-
-#[derive(Default, Clone, Resource)]
-struct SwarmState {}
-
-#[tokio::main]
-async fn main() {
- {
- use parking_lot::deadlock;
- use std::thread;
- use std::time::Duration;
- // Create a background thread which checks for deadlocks every 10s
- thread::spawn(move || loop {
- thread::sleep(Duration::from_secs(10));
- let deadlocks = deadlock::check_deadlock();
- if deadlocks.is_empty() {
- continue;
- }
-
- println!("{} deadlocks detected", deadlocks.len());
- for (i, threads) in deadlocks.iter().enumerate() {
- println!("Deadlock #{i}");
- for t in threads {
- println!("Thread Id {:#?}", t.thread_id());
- println!("{:#?}", t.backtrace());
- }
- }
- });
- }
-
- let mut accounts = Vec::new();
-
- for i in 0..1 {
- accounts.push(Account::offline(&format!("bot{i}")));
- }
-
- SwarmBuilder::new()
- .add_accounts(accounts.clone())
- .set_handler(handle)
- .set_swarm_handler(swarm_handle)
- .join_delay(Duration::from_millis(100))
- .start("localhost")
- .await
- .unwrap();
-}
-
-async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<()> {
- match event {
- Event::Init => {
- // bot.set_client_information(azalea_client::ClientInformation {
- // view_distance: 2,
- // ..Default::default()
- // })
- // .await?;
- }
- Event::Login => {
- bot.chat("Hello world");
- }
- Event::Chat(m) => {
- // println!("client chat message: {}", m.content());
- if m.content() == bot.profile.name {
- bot.chat("Bye");
- tokio::time::sleep(Duration::from_millis(50)).await;
- bot.disconnect();
- }
- let Some(sender) = m.username() else {
- return Ok(());
- };
- // let mut ecs = bot.ecs.lock();
- // let entity = bot
- // .ecs
- // .lock()
- // .query::<&Player>()
- // .iter(&mut ecs)
- // .find(|e| e.name() == Some(sender));
- // let entity = bot.entity_by::<With<Player>>(|name: &Name| name == sender);
- let entity = bot.entity_by::<With<Player>, (&GameProfileComponent,)>(
- |(profile,): &(&GameProfileComponent,)| profile.name == sender,
- );
- match m.content().as_str() {
- "whereami" => {
- let Some(entity) = entity else {
- bot.chat("I can't see you");
- return Ok(());
- };
- let pos = bot.entity_component::<Position>(entity);
- bot.chat(&format!("You're at {pos:?}"));
- }
- "whereareyou" => {
- let pos = bot.position();
- bot.chat(&format!("I'm at {pos:?}"));
- }
- "goto" => {
- let Some(entity) = entity else {
- bot.chat("I can't see you");
- return Ok(());
- };
- let entity_pos = bot.entity_component::<Position>(entity);
- let target_pos: BlockPos = entity_pos.into();
- println!("going to {target_pos:?}");
- bot.goto(BlockPosGoal(target_pos));
- }
- "worldborder" => {
- bot.goto(BlockPosGoal(BlockPos::new(30_000_000, 70, 0)));
- }
- "look" => {
- let Some(entity) = entity else {
- bot.chat("I can't see you");
- return Ok(());
- };
- let entity_pos = bot
- .entity_component::<Position>(entity)
- .up(bot.entity_component::<EyeHeight>(entity).into());
- println!("entity_pos: {entity_pos:?}");
- bot.look_at(entity_pos);
- }
- "jump" => {
- bot.set_jumping(true);
- }
- "walk" => {
- bot.walk(WalkDirection::Forward);
- }
- "sprint" => {
- bot.sprint(SprintDirection::Forward);
- }
- "stop" => {
- bot.set_jumping(false);
- bot.walk(WalkDirection::None);
- }
- "lag" => {
- std::thread::sleep(Duration::from_millis(1000));
- }
- "quit" => {
- bot.disconnect();
- tokio::time::sleep(Duration::from_millis(1000)).await;
- std::process::exit(0);
- }
- "inventory" => {
- println!("inventory: {:?}", bot.menu());
- }
- "findblock" => {
- let target_pos = bot.world().read().find_block(
- bot.position(),
- &azalea::registry::Block::DiamondBlock.into(),
- );
- bot.chat(&format!("target_pos: {target_pos:?}",));
- }
- "gotoblock" => {
- let target_pos = bot.world().read().find_block(
- bot.position(),
- &azalea::registry::Block::DiamondBlock.into(),
- );
- if let Some(target_pos) = target_pos {
- // +1 to stand on top of the block
- bot.goto(BlockPosGoal(target_pos.up(1)));
- } else {
- bot.chat("no diamond block found");
- }
- }
- "mineblock" => {
- let target_pos = bot.world().read().find_block(
- bot.position(),
- &azalea::registry::Block::DiamondBlock.into(),
- );
- if let Some(target_pos) = target_pos {
- // +1 to stand on top of the block
- bot.chat("ok mining diamond block");
- bot.look_at(target_pos.center());
- bot.mine(target_pos).await;
- bot.chat("finished mining");
- } else {
- bot.chat("no diamond block found");
- }
- }
- "lever" => {
- let target_pos = bot
- .world()
- .read()
- .find_block(bot.position(), &azalea::registry::Block::Lever.into());
- let Some(target_pos) = target_pos else {
- bot.chat("no lever found");
- return Ok(());
- };
- bot.goto(BlockPosGoal(target_pos));
- bot.look_at(target_pos.center());
- bot.block_interact(target_pos);
- }
- "hitresult" => {
- let hit_result = bot.get_component::<HitResultComponent>();
- bot.chat(&format!("hit_result: {hit_result:?}",));
- }
- "chest" => {
- let target_pos = bot
- .world()
- .read()
- .find_block(bot.position(), &azalea::registry::Block::Chest.into());
- let Some(target_pos) = target_pos else {
- bot.chat("no chest found");
- return Ok(());
- };
- bot.look_at(target_pos.center());
- let container = bot.open_container_at(target_pos).await;
- println!("container: {container:?}");
- if let Some(container) = container {
- if let Some(contents) = container.contents() {
- for item in contents {
- if let ItemSlot::Present(item) = item {
- println!("item: {item:?}");
- }
- }
- } else {
- println!("container was immediately closed");
- }
- } else {
- println!("no container found");
- }
- }
- "attack" => {
- let mut nearest_entity = None;
- let mut nearest_distance = f64::INFINITY;
- let mut nearest_pos = Vec3::default();
- let bot_position = bot.position();
- let bot_entity = bot.entity;
- let bot_instance_name = bot.component::<InstanceName>();
- {
- let mut ecs = bot.ecs.lock();
- let mut query = ecs.query_filtered::<(
- azalea::ecs::entity::Entity,
- &MinecraftEntityId,
- &Position,
- &InstanceName,
- &EyeHeight,
- ), With<MinecraftEntityId>>();
- for (entity, &entity_id, position, instance_name, eye_height) in
- query.iter(&ecs)
- {
- if entity == bot_entity {
- continue;
- }
- if instance_name != &bot_instance_name {
- continue;
- }
-
- let distance = bot_position.distance_to(position);
- if distance < 4.0 && distance < nearest_distance {
- nearest_entity = Some(entity_id);
- nearest_distance = distance;
- nearest_pos = position.up(**eye_height as f64);
- }
- }
- }
- if let Some(nearest_entity) = nearest_entity {
- bot.look_at(nearest_pos);
- bot.attack(nearest_entity);
- bot.chat("attacking");
- let mut ticks = bot.get_tick_broadcaster();
- while ticks.recv().await.is_ok() {
- if !bot.has_attack_cooldown() {
- break;
- }
- }
- bot.chat("finished attacking");
- } else {
- bot.chat("no entities found");
- }
- }
- "heightmap" => {
- let position = bot.position();
- let chunk_pos = ChunkPos::from(position);
- let chunk_block_pos = ChunkBlockPos::from(position);
- let chunk = bot.world().read().chunks.get(&chunk_pos);
- if let Some(chunk) = chunk {
- let heightmaps = &chunk.read().heightmaps;
- let Some(world_surface_heightmap) =
- heightmaps.get(&HeightmapKind::WorldSurface)
- else {
- bot.chat("no world surface heightmap");
- return Ok(());
- };
- let highest_y = world_surface_heightmap
- .get_highest_taken(chunk_block_pos.x, chunk_block_pos.z);
- bot.chat(&format!("highest_y: {highest_y}",));
- } else {
- bot.chat("no chunk found");
- }
- }
- "debugblock" => {
- // send the block that we're standing on
- let block_pos = BlockPos::from(bot.position().down(0.1));
- let block = bot.world().read().get_block_state(&block_pos);
- bot.chat(&format!("block: {block:?}"));
- }
- "debugchunks" => {
- {
- println!("shared:");
-
- let mut ecs = bot.ecs.lock();
-
- let instance_holder = bot.query::<&InstanceHolder>(&mut ecs).clone();
- drop(ecs);
- let local_chunk_storage = &instance_holder.partial_instance.read().chunks;
- let shared_chunk_storage = instance_holder.instance.read();
-
- let mut total_loaded_chunks_count = 0;
- for (chunk_pos, chunk) in &shared_chunk_storage.chunks.map {
- if let Some(chunk) = chunk.upgrade() {
- let in_range = local_chunk_storage.in_range(chunk_pos);
- println!(
- "{chunk_pos:?} has {} references{}",
- std::sync::Arc::strong_count(&chunk) - 1,
- if in_range { "" } else { " (out of range)" }
- );
- total_loaded_chunks_count += 1;
- }
- }
-
- println!("local:");
- println!("view range: {}", local_chunk_storage.view_range());
- println!("view center: {:?}", local_chunk_storage.view_center());
-
- let mut local_loaded_chunks_count = 0;
- for (i, chunk) in local_chunk_storage.chunks().enumerate() {
- if let Some(chunk) = chunk {
- let chunk_pos = local_chunk_storage.chunk_pos_from_index(i);
- println!(
- "{chunk_pos:?} (#{i}) has {} references",
- std::sync::Arc::strong_count(&chunk)
- );
- local_loaded_chunks_count += 1;
- }
- }
-
- println!("total loaded chunks: {total_loaded_chunks_count}");
- println!(
- "local loaded chunks: {local_loaded_chunks_count}/{}",
- local_chunk_storage.chunks().collect::<Vec<_>>().len()
- );
- }
- {
- let local_chunk_storage_lock = bot.partial_world();
- let local_chunk_storage = local_chunk_storage_lock.read();
- let current_chunk_loaded = local_chunk_storage
- .chunks
- .limited_get(&ChunkPos::from(bot.position()));
-
- bot.chat(&format!(
- "current chunk loaded: {}",
- current_chunk_loaded.is_some()
- ));
- }
- }
- _ => {}
- }
- }
- Event::Packet(packet) => {
- if let ClientboundGamePacket::Login(_) = *packet {
- println!("login packet");
- }
- }
- Event::Disconnect(reason) => {
- if let Some(reason) = reason {
- println!("bot got kicked for reason: {}", reason.to_ansi());
- } else {
- println!("bot got kicked");
- }
- }
- _ => {}
- }
-
- Ok(())
-}
-
-async fn swarm_handle(
- mut swarm: Swarm,
- event: SwarmEvent,
- _state: SwarmState,
-) -> anyhow::Result<()> {
- match &event {
- SwarmEvent::Disconnect(account) => {
- println!("bot got kicked! {}", account.username);
- tokio::time::sleep(Duration::from_secs(5)).await;
- swarm.add_and_retry_forever(account, State::default()).await;
- }
- SwarmEvent::Chat(m) => {
- println!("swarm chat message: {}", m.message().to_ansi());
- if m.message().to_string() == "<py5> world" {
- for (name, world) in &swarm.instance_container.read().instances {
- println!("world name: {name}");
- if let Some(w) = world.upgrade() {
- for chunk_pos in w.read().chunks.map.values() {
- println!("chunk: {chunk_pos:?}");
- }
- } else {
- println!("nvm world is gone");
- }
- }
- }
- if m.message().to_string() == "<py5> hi" {
- for bot in swarm {
- bot.chat("hello");
- }
- }
- }
- _ => {}
- }
- Ok(())
-}
diff --git a/azalea/examples/testbot/commands.rs b/azalea/examples/testbot/commands.rs
new file mode 100644
index 00000000..db606aa6
--- /dev/null
+++ b/azalea/examples/testbot/commands.rs
@@ -0,0 +1,46 @@
+pub mod combat;
+pub mod debug;
+pub mod movement;
+
+use azalea::brigadier::prelude::*;
+use azalea::chat::ChatPacket;
+use azalea::ecs::prelude::Entity;
+use azalea::ecs::prelude::*;
+use azalea::entity::metadata::Player;
+use azalea::Client;
+use azalea::GameProfileComponent;
+use parking_lot::Mutex;
+
+use crate::State;
+
+pub type Ctx = CommandContext<Mutex<CommandSource>>;
+
+pub struct CommandSource {
+ pub bot: Client,
+ pub state: State,
+ pub chat: ChatPacket,
+}
+
+impl CommandSource {
+ pub fn reply(&self, message: &str) {
+ if self.chat.is_whisper() {
+ self.bot
+ .chat(&format!("/w {} {}", self.chat.username().unwrap(), message));
+ } else {
+ self.bot.chat(message);
+ }
+ }
+
+ pub fn entity(&mut self) -> Option<Entity> {
+ let username = self.chat.username()?;
+ self.bot.entity_by::<With<Player>, &GameProfileComponent>(
+ |profile: &&GameProfileComponent| profile.name == username,
+ )
+ }
+}
+
+pub fn register_commands(commands: &mut CommandDispatcher<Mutex<CommandSource>>) {
+ combat::register(commands);
+ debug::register(commands);
+ movement::register(commands);
+}
diff --git a/azalea/examples/testbot/commands/combat.rs b/azalea/examples/testbot/commands/combat.rs
new file mode 100644
index 00000000..b440b3ac
--- /dev/null
+++ b/azalea/examples/testbot/commands/combat.rs
@@ -0,0 +1,26 @@
+use azalea::brigadier::prelude::*;
+use parking_lot::Mutex;
+
+use super::{CommandSource, Ctx};
+
+pub fn register(commands: &mut CommandDispatcher<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
+ })),
+ );
+}
diff --git a/azalea/examples/testbot/commands/debug.rs b/azalea/examples/testbot/commands/debug.rs
new file mode 100644
index 00000000..ae0808cb
--- /dev/null
+++ b/azalea/examples/testbot/commands/debug.rs
@@ -0,0 +1,105 @@
+//! Commands for debugging and getting the current state of the bot.
+
+use azalea::{
+ brigadier::prelude::*,
+ entity::{LookDirection, Position},
+ interact::HitResultComponent,
+ world::MinecraftEntityId,
+};
+use parking_lot::Mutex;
+
+use super::{CommandSource, Ctx};
+
+pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) {
+ commands.register(literal("ping").executes(|ctx: &Ctx| {
+ let source = ctx.source.lock();
+ source.reply("pong!");
+ 1
+ }));
+
+ commands.register(literal("whereami").executes(|ctx: &Ctx| {
+ let mut source = ctx.source.lock();
+ let Some(entity) = source.entity() else {
+ source.reply("You aren't in render distance!");
+ return 0;
+ };
+ let position = source.bot.entity_component::<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.miss {
+ source.reply("I'm not looking at anything");
+ return 1;
+ }
+
+ let block_pos = hit_result.block_pos;
+ let block = source.bot.world().read().get_block_state(&block_pos);
+
+ source.reply(&format!("I'm looking at {block:?} at {block_pos:?}"));
+
+ 1
+ }));
+}
diff --git a/azalea/examples/testbot/commands/movement.rs b/azalea/examples/testbot/commands/movement.rs
new file mode 100644
index 00000000..4957533f
--- /dev/null
+++ b/azalea/examples/testbot/commands/movement.rs
@@ -0,0 +1,191 @@
+use std::time::Duration;
+
+use azalea::{
+ brigadier::prelude::*,
+ entity::{EyeHeight, Position},
+ pathfinder::goals::{BlockPosGoal, XZGoal},
+ prelude::*,
+ BlockPos, SprintDirection, WalkDirection,
+};
+use parking_lot::Mutex;
+
+use crate::BotTask;
+
+use super::{CommandSource, Ctx};
+
+pub fn register(commands: &mut CommandDispatcher<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(argument("x", integer()).then(argument("y", integer()).then(
+ argument("z", integer()).executes(|ctx: &Ctx| {
+ let source = ctx.source.lock();
+ let x = get_integer(ctx, "x").unwrap();
+ let y = get_integer(ctx, "y").unwrap();
+ let z = get_integer(ctx, "z").unwrap();
+ println!("goto xyz {x} {y} {z}");
+ source.reply("ok");
+ source.bot.goto(BlockPosGoal(BlockPos::new(x, y, z)));
+ 1
+ }),
+ ))),
+ );
+
+ commands.register(literal("down").executes(|ctx: &Ctx| {
+ let source = ctx.source.clone();
+ tokio::spawn(async move {
+ let mut bot = source.lock().bot.clone();
+ let position = BlockPos::from(bot.position());
+ source.lock().reply("mining...");
+ bot.mine(position.down(1)).await;
+ source.lock().reply("done");
+ });
+ 1
+ }));
+
+ commands.register(
+ literal("look")
+ .executes(|ctx: &Ctx| {
+ // look for the sender
+ let mut source = ctx.source.lock();
+ let Some(entity) = source.entity() else {
+ source.reply("I can't see you!");
+ return 0;
+ };
+ let Some(position) = source.bot.get_entity_component::<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 mut source = ctx.source.lock();
+ source.bot.look_at(pos.center());
+ 1
+ }),
+ ))),
+ );
+
+ commands.register(
+ literal("walk").then(argument("seconds", float()).executes(|ctx: &Ctx| {
+ let mut seconds = get_float(ctx, "seconds").unwrap();
+ let source = ctx.source.lock();
+ let mut bot = source.bot.clone();
+
+ if seconds < 0. {
+ bot.walk(WalkDirection::Backward);
+ seconds = -seconds;
+ } else {
+ bot.walk(WalkDirection::Forward);
+ }
+
+ tokio::spawn(async move {
+ tokio::time::sleep(Duration::from_secs_f32(seconds)).await;
+ bot.walk(WalkDirection::None);
+ });
+ source.reply(&format!("ok, walking for {seconds} seconds"));
+ 1
+ })),
+ );
+ commands.register(
+ literal("sprint").then(argument("seconds", float()).executes(|ctx: &Ctx| {
+ let seconds = get_float(ctx, "seconds").unwrap();
+ let source = ctx.source.lock();
+ let mut bot = source.bot.clone();
+ bot.sprint(SprintDirection::Forward);
+ tokio::spawn(async move {
+ tokio::time::sleep(Duration::from_secs_f32(seconds)).await;
+ bot.walk(WalkDirection::None);
+ });
+ source.reply(&format!("ok, spriting for {seconds} seconds"));
+ 1
+ })),
+ );
+
+ commands.register(literal("north").executes(|ctx: &Ctx| {
+ let mut source = ctx.source.lock();
+ source.bot.set_direction(180., 0.);
+ source.reply("ok");
+ 1
+ }));
+ commands.register(literal("south").executes(|ctx: &Ctx| {
+ let mut source = ctx.source.lock();
+ source.bot.set_direction(0., 0.);
+ source.reply("ok");
+ 1
+ }));
+ commands.register(literal("east").executes(|ctx: &Ctx| {
+ let mut source = ctx.source.lock();
+ source.bot.set_direction(-90., 0.);
+ source.reply("ok");
+ 1
+ }));
+ commands.register(literal("west").executes(|ctx: &Ctx| {
+ let mut source = ctx.source.lock();
+ source.bot.set_direction(90., 0.);
+ source.reply("ok");
+ 1
+ }));
+ commands.register(
+ literal("jump")
+ .executes(|ctx: &Ctx| {
+ let mut source = ctx.source.lock();
+ source.bot.jump();
+ source.reply("ok");
+ 1
+ })
+ .then(argument("enabled", bool()).executes(|ctx: &Ctx| {
+ let jumping = get_bool(ctx, "enabled").unwrap();
+ let mut source = ctx.source.lock();
+ source.bot.set_jumping(jumping);
+ 1
+ })),
+ );
+
+ commands.register(literal("stop").executes(|ctx: &Ctx| {
+ let source = ctx.source.lock();
+ source.bot.stop_pathfinding();
+ source.reply("ok");
+ *source.state.task.lock() = BotTask::None;
+ 1
+ }));
+}
diff --git a/azalea/examples/testbot/killaura.rs b/azalea/examples/testbot/killaura.rs
new file mode 100644
index 00000000..d86356fe
--- /dev/null
+++ b/azalea/examples/testbot/killaura.rs
@@ -0,0 +1,48 @@
+use azalea::{
+ ecs::prelude::*,
+ entity::{metadata::AbstractMonster, Dead, LocalEntity, Position},
+ prelude::*,
+ world::{InstanceName, MinecraftEntityId},
+};
+
+use crate::State;
+
+pub fn tick(mut bot: Client, state: State) -> anyhow::Result<()> {
+ if !state.killaura {
+ return Ok(());
+ }
+ if bot.has_attack_cooldown() {
+ return Ok(());
+ }
+ let mut nearest_entity = None;
+ let mut nearest_distance = f64::INFINITY;
+ let bot_position = bot.eye_position();
+ let bot_instance_name = bot.component::<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(())
+}
diff --git a/azalea/examples/testbot/main.rs b/azalea/examples/testbot/main.rs
new file mode 100644
index 00000000..9a3bd4f8
--- /dev/null
+++ b/azalea/examples/testbot/main.rs
@@ -0,0 +1,198 @@
+//! A relatively simple bot for demonstrating some of Azalea's capabilities.
+//!
+//! Usage:
+//! - Modify the consts below if necessary.
+//! - Run `cargo r --example testbot`
+//! - Commands are prefixed with `!` in chat. You can send them either in public
+//! chat or as a /msg.
+//! - Some commands to try are `!goto`, `!killaura`, `!down`. Check the
+//! `commands` directory to see all of them.
+
+#![feature(async_closure)]
+#![feature(trivial_bounds)]
+
+mod commands;
+pub mod killaura;
+
+use azalea::pathfinder::PathfinderDebugParticles;
+use azalea::{Account, ClientInformation};
+
+use azalea::brigadier::command_dispatcher::CommandDispatcher;
+use azalea::ecs::prelude::*;
+use azalea::prelude::*;
+use azalea::swarm::prelude::*;
+use commands::{register_commands, CommandSource};
+use parking_lot::Mutex;
+use std::sync::Arc;
+use std::time::Duration;
+
+const USERNAME: &str = "azalea";
+const ADDRESS: &str = "localhost";
+/// Whether the bot should run /particle a ton of times to show where it's
+/// pathfinding to. You should only have this on if the bot has operator
+/// permissions, otherwise it'll just spam the server console unnecessarily.
+const PATHFINDER_DEBUG_PARTICLES: bool = true;
+
+#[tokio::main]
+async fn main() {
+ {
+ use parking_lot::deadlock;
+ use std::thread;
+ use std::time::Duration;
+
+ // Create a background thread which checks for deadlocks every 10s
+ thread::spawn(move || loop {
+ thread::sleep(Duration::from_secs(10));
+ let deadlocks = deadlock::check_deadlock();
+ if deadlocks.is_empty() {
+ continue;
+ }
+
+ println!("{} deadlocks detected", deadlocks.len());
+ for (i, threads) in deadlocks.iter().enumerate() {
+ println!("Deadlock #{i}");
+ for t in threads {
+ println!("Thread Id {:#?}", t.thread_id());
+ println!("{:#?}", t.backtrace());
+ }
+ }
+ });
+ }
+
+ let account = Account::offline(USERNAME);
+
+ let mut commands = CommandDispatcher::new();
+ register_commands(&mut commands);
+ let commands = Arc::new(commands);
+
+ let builder = SwarmBuilder::new();
+ builder
+ .set_handler(handle)
+ .set_swarm_handler(swarm_handle)
+ .add_account_with_state(
+ account,
+ State {
+ commands: commands.clone(),
+ ..Default::default()
+ },
+ )
+ .join_delay(Duration::from_millis(100))
+ .start(ADDRESS)
+ .await
+ .unwrap();
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
+pub enum BotTask {
+ #[default]
+ None,
+}
+
+#[derive(Component, Clone)]
+pub struct State {
+ pub commands: Arc<CommandDispatcher<Mutex<CommandSource>>>,
+ pub killaura: bool,
+ pub task: Arc<Mutex<BotTask>>,
+}
+
+impl Default for State {
+ fn default() -> Self {
+ Self {
+ commands: Arc::new(CommandDispatcher::new()),
+ killaura: true,
+ task: Arc::new(Mutex::new(BotTask::None)),
+ }
+ }
+}
+
+#[derive(Resource, Default, Clone)]
+struct SwarmState;
+
+async fn handle(bot: Client, event: azalea::Event, state: State) -> anyhow::Result<()> {
+ match event {
+ azalea::Event::Init => {
+ bot.set_client_information(ClientInformation {
+ view_distance: 32,
+ ..Default::default()
+ })
+ .await?;
+ if PATHFINDER_DEBUG_PARTICLES {
+ bot.ecs
+ .lock()
+ .entity_mut(bot.entity)
+ .insert(PathfinderDebugParticles);
+ }
+ }
+ azalea::Event::Login => {}
+ azalea::Event::Chat(chat) => {
+ let (Some(username), content) = chat.split_sender_and_content() else {
+ return Ok(());
+ };
+ if username != "py5" {
+ return Ok(());
+ }
+
+ println!("{:?}", chat.message());
+
+ let command = if chat.is_whisper() {
+ Some(content)
+ } else {
+ content.strip_prefix("!").map(|s| s.to_owned())
+ };
+ if let Some(command) = command {
+ match state.commands.execute(
+ command,
+ Mutex::new(CommandSource {
+ bot: bot.clone(),
+ chat: chat.clone(),
+ state: state.clone(),
+ }),
+ ) {
+ Ok(_) => {}
+ Err(err) => {
+ eprintln!("{err:?}");
+ let command_source = CommandSource {
+ bot,
+ chat: chat.clone(),
+ state: state.clone(),
+ };
+ command_source.reply(&format!("{err:?}"));
+ }
+ }
+ }
+ }
+ azalea::Event::Tick => {
+ killaura::tick(bot.clone(), state.clone())?;
+
+ let task = state.task.lock().clone();
+ match task {
+ BotTask::None => {}
+ }
+ }
+ _ => {}
+ }
+
+ Ok(())
+}
+async fn swarm_handle(
+ mut swarm: Swarm,
+ event: SwarmEvent,
+ _state: SwarmState,
+) -> anyhow::Result<()> {
+ match &event {
+ SwarmEvent::Disconnect(account) => {
+ println!("bot got kicked! {}", account.username);
+ tokio::time::sleep(Duration::from_secs(5)).await;
+ swarm.add_and_retry_forever(account, State::default()).await;
+ }
+ SwarmEvent::Chat(chat) => {
+ if chat.message().to_string() == "The particle was not visible for anybody" {
+ return Ok(());
+ }
+ println!("{}", chat.message().to_ansi());
+ }
+ _ => {}
+ }
+
+ Ok(())
+}