diff options
Diffstat (limited to 'azalea/examples')
| -rw-r--r-- | azalea/examples/testbot.rs | 424 | ||||
| -rw-r--r-- | azalea/examples/testbot/commands.rs | 46 | ||||
| -rw-r--r-- | azalea/examples/testbot/commands/combat.rs | 26 | ||||
| -rw-r--r-- | azalea/examples/testbot/commands/debug.rs | 105 | ||||
| -rw-r--r-- | azalea/examples/testbot/commands/movement.rs | 191 | ||||
| -rw-r--r-- | azalea/examples/testbot/killaura.rs | 48 | ||||
| -rw-r--r-- | azalea/examples/testbot/main.rs | 198 |
7 files changed, 614 insertions, 424 deletions
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(()) +} |
