1
2
Fork 0
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:
mat 2024-01-07 21:50:38 -06:00
parent 5ea1271145
commit 0aa439d5ca
8 changed files with 615 additions and 425 deletions

View file

@ -53,7 +53,7 @@ async fn main() {
ClientBuilder::new()
.set_handler(handle)
.start(account.clone(), "localhost")
.start(account, "localhost")
.await
.unwrap();
}

View file

@ -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(())
}

View 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);
}

View 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
})),
);
}

View 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
}));
}

View 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
}));
}

View 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(())
}

View 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(())
}