mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 06:16:04 +00:00
rewrite testbot to use brigadier
This commit is contained in:
parent
5ea1271145
commit
0aa439d5ca
8 changed files with 615 additions and 425 deletions
|
@ -53,7 +53,7 @@ async fn main() {
|
|||
|
||||
ClientBuilder::new()
|
||||
.set_handler(handle)
|
||||
.start(account.clone(), "localhost")
|
||||
.start(account, "localhost")
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
46
azalea/examples/testbot/commands.rs
Normal file
46
azalea/examples/testbot/commands.rs
Normal file
|
@ -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);
|
||||
}
|
26
azalea/examples/testbot/commands/combat.rs
Normal file
26
azalea/examples/testbot/commands/combat.rs
Normal file
|
@ -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
|
||||
})),
|
||||
);
|
||||
}
|
105
azalea/examples/testbot/commands/debug.rs
Normal file
105
azalea/examples/testbot/commands/debug.rs
Normal file
|
@ -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
|
||||
}));
|
||||
}
|
191
azalea/examples/testbot/commands/movement.rs
Normal file
191
azalea/examples/testbot/commands/movement.rs
Normal file
|
@ -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
|
||||
}));
|
||||
}
|
48
azalea/examples/testbot/killaura.rs
Normal file
48
azalea/examples/testbot/killaura.rs
Normal file
|
@ -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(())
|
||||
}
|
198
azalea/examples/testbot/main.rs
Normal file
198
azalea/examples/testbot/main.rs
Normal file
|
@ -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(())
|
||||
}
|
Loading…
Add table
Reference in a new issue