aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xCargo.lock50
-rw-r--r--README.md3
-rwxr-xr-xazalea-client/Cargo.toml1
-rw-r--r--azalea-client/src/client.rs72
-rw-r--r--azalea-client/src/movement.rs54
-rw-r--r--azalea/Cargo.toml6
-rw-r--r--azalea/README.md4
-rw-r--r--azalea/examples/craft_dig_straight_down.rs73
-rw-r--r--azalea/examples/echo.rs60
-rw-r--r--azalea/examples/mine_a_chunk.rs70
-rw-r--r--azalea/examples/potatobot/autoeat.rs23
-rw-r--r--azalea/examples/potatobot/main.rs56
-rw-r--r--azalea/examples/pvp.rs42
-rw-r--r--azalea/src/bot.rs42
-rw-r--r--azalea/src/lib.rs82
-rwxr-xr-xbot/Cargo.toml6
-rw-r--r--bot/src/main.rs44
17 files changed, 465 insertions, 223 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 00ac6582..ff10db62 100755
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -72,9 +72,9 @@ dependencies = [
[[package]]
name = "async-trait"
-version = "0.1.56"
+version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716"
+checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f"
dependencies = [
"proc-macro2",
"quote",
@@ -103,8 +103,12 @@ name = "azalea"
version = "0.1.0"
dependencies = [
"anyhow",
+ "async-trait",
"azalea-client",
+ "azalea-protocol",
"env_logger",
+ "parking_lot 0.12.1",
+ "thiserror",
"tokio",
]
@@ -183,6 +187,7 @@ dependencies = [
"azalea-protocol",
"azalea-world",
"log",
+ "parking_lot 0.12.1",
"thiserror",
"tokio",
"uuid",
@@ -338,11 +343,9 @@ name = "bot"
version = "0.1.0"
dependencies = [
"anyhow",
- "azalea-client",
- "azalea-core",
- "azalea-physics",
- "azalea-protocol",
+ "azalea",
"env_logger",
+ "parking_lot 0.12.1",
"tokio",
"uuid",
]
@@ -1029,7 +1032,17 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
- "parking_lot_core",
+ "parking_lot_core 0.8.5",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core 0.9.3",
]
[[package]]
@@ -1047,6 +1060,19 @@ dependencies = [
]
[[package]]
+name = "parking_lot_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-sys",
+]
+
+[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1393,18 +1419,18 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "1.0.34"
+version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c1b05ca9d106ba7d2e31a9dab4a64e7be2cce415321966ea3132c49a656e252"
+checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
-version = "1.0.34"
+version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8f2591983642de85c921015f3f070c665a197ed69e417af436115e3a1407487"
+checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
dependencies = [
"proc-macro2",
"quote",
@@ -1517,7 +1543,7 @@ dependencies = [
"lazy_static",
"log",
"lru-cache",
- "parking_lot",
+ "parking_lot 0.11.2",
"resolv-conf",
"smallvec",
"thiserror",
diff --git a/README.md b/README.md
index 3b043796..7b7be3e1 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,8 @@ I wanted a fun excuse to do something cool with Rust, and I also felt like I cou
- Do everything a vanilla client can do.
- Be intuitive and easy to use.
-- Bypass most/all anticheats.
+- Make it easy to have many bots working at the same time.
+- Don't trigger anticheats.
- Support the latest Minecraft version.
- Be fast and memory efficient.
diff --git a/azalea-client/Cargo.toml b/azalea-client/Cargo.toml
index 66e5ca42..bbcf732b 100755
--- a/azalea-client/Cargo.toml
+++ b/azalea-client/Cargo.toml
@@ -16,6 +16,7 @@ azalea-physics = {path = "../azalea-physics"}
azalea-protocol = {path = "../azalea-protocol"}
azalea-world = {path = "../azalea-world"}
log = "0.4.17"
+parking_lot = "0.12.1"
thiserror = "^1.0.34"
tokio = {version = "^1.19.2", features = ["sync"]}
uuid = "^1.1.2"
diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs
index 2b721206..d5071787 100644
--- a/azalea-client/src/client.rs
+++ b/azalea-client/src/client.rs
@@ -25,14 +25,13 @@ use azalea_protocol::{
read::ReadPacketError,
resolver, ServerAddress,
};
-use azalea_world::entity::EntityData;
-use azalea_world::Dimension;
-use log::{debug, error, warn};
-use std::{
- fmt::Debug,
- io,
- sync::{Arc, Mutex},
+use azalea_world::{
+ entity::{EntityData, EntityMut, EntityRef},
+ Dimension,
};
+use log::{debug, error, warn};
+use parking_lot::Mutex;
+use std::{fmt::Debug, io, sync::Arc};
use thiserror::Error;
use tokio::{
io::AsyncWriteExt,
@@ -41,12 +40,14 @@ use tokio::{
time::{self},
};
+/// Events are sent before they're processed, so for example game ticks happen
+/// at the beginning of a tick before anything has happened.
#[derive(Debug, Clone)]
pub enum Event {
Login,
Chat(ChatPacket),
/// A game tick, happens 20 times per second.
- GameTick,
+ Tick,
Packet(Box<ClientboundGamePacket>),
}
@@ -219,7 +220,7 @@ impl Client {
// read the error to see where the issue is
// you might be able to just drop the lock or put it in its own scope to fix
{
- let mut tasks = client.tasks.lock().unwrap();
+ let mut tasks = client.tasks.lock();
tasks.push(tokio::spawn(Self::protocol_loop(
client.clone(),
tx.clone(),
@@ -238,7 +239,7 @@ impl Client {
/// Disconnect from the server, ending all tasks.
pub async fn shutdown(self) -> Result<(), std::io::Error> {
self.write_conn.lock().await.write_stream.shutdown().await?;
- let tasks = self.tasks.lock().unwrap();
+ let tasks = self.tasks.lock();
for task in tasks.iter() {
task.abort();
}
@@ -346,7 +347,7 @@ impl Client {
.as_int()
.expect("min_y tag is not an int");
- let mut dimension_lock = client.dimension.lock().unwrap();
+ let mut dimension_lock = client.dimension.lock();
// the 16 here is our render distance
// i'll make this an actual setting later
*dimension_lock = Dimension::new(16, height, min_y);
@@ -354,7 +355,7 @@ impl Client {
let entity = EntityData::new(client.game_profile.uuid, Vec3::default());
dimension_lock.add_entity(p.player_id, entity);
- let mut player_lock = client.player.lock().unwrap();
+ let mut player_lock = client.player.lock();
player_lock.set_entity_id(p.player_id);
}
@@ -411,11 +412,11 @@ impl Client {
let (new_pos, y_rot, x_rot) = {
let player_entity_id = {
- let player_lock = client.player.lock().unwrap();
+ let player_lock = client.player.lock();
player_lock.entity_id
};
- let mut dimension_lock = client.dimension.lock().unwrap();
+ let mut dimension_lock = client.dimension.lock();
let mut player_entity = dimension_lock
.entity_mut(player_entity_id)
@@ -503,7 +504,7 @@ impl Client {
debug!("Got chunk cache center packet {:?}", p);
client
.dimension
- .lock()?
+ .lock()
.update_view_center(&ChunkPos::new(p.x, p.z));
}
ClientboundGamePacket::LevelChunkWithLight(p) => {
@@ -513,7 +514,7 @@ impl Client {
// debug("chunk {:?}")
client
.dimension
- .lock()?
+ .lock()
.replace_with_packet_data(&pos, &mut p.chunk_data.data.as_slice())
.unwrap();
}
@@ -523,7 +524,7 @@ impl Client {
ClientboundGamePacket::AddEntity(p) => {
debug!("Got add entity packet {:?}", p);
let entity = EntityData::from(p);
- client.dimension.lock()?.add_entity(p.id, entity);
+ client.dimension.lock().add_entity(p.id, entity);
}
ClientboundGamePacket::SetEntityData(_p) => {
// debug!("Got set entity data packet {:?}", p);
@@ -540,7 +541,7 @@ impl Client {
ClientboundGamePacket::AddPlayer(p) => {
debug!("Got add player packet {:?}", p);
let entity = EntityData::from(p);
- client.dimension.lock()?.add_entity(p.id, entity);
+ client.dimension.lock().add_entity(p.id, entity);
}
ClientboundGamePacket::InitializeBorder(p) => {
debug!("Got initialize border packet {:?}", p);
@@ -561,7 +562,7 @@ impl Client {
debug!("Got set experience packet {:?}", p);
}
ClientboundGamePacket::TeleportEntity(p) => {
- let mut dimension_lock = client.dimension.lock()?;
+ let mut dimension_lock = client.dimension.lock();
dimension_lock
.set_entity_pos(
@@ -581,14 +582,14 @@ impl Client {
// debug!("Got rotate head packet {:?}", p);
}
ClientboundGamePacket::MoveEntityPos(p) => {
- let mut dimension_lock = client.dimension.lock()?;
+ let mut dimension_lock = client.dimension.lock();
dimension_lock
.move_entity_with_delta(p.entity_id, &p.delta)
.map_err(|e| HandleError::Other(e.into()))?;
}
ClientboundGamePacket::MoveEntityPosRot(p) => {
- let mut dimension_lock = client.dimension.lock()?;
+ let mut dimension_lock = client.dimension.lock();
dimension_lock
.move_entity_with_delta(p.entity_id, &p.delta)
@@ -623,7 +624,7 @@ impl Client {
}
ClientboundGamePacket::BlockUpdate(p) => {
debug!("Got block update packet {:?}", p);
- let mut dimension = client.dimension.lock()?;
+ let mut dimension = client.dimension.lock();
dimension.set_block_state(&p.pos, p.block_state);
}
ClientboundGamePacket::Animate(p) => {
@@ -725,10 +726,12 @@ impl Client {
/// Runs every 50 milliseconds.
async fn game_tick(client: &mut Client, tx: &UnboundedSender<Event>) {
+ tx.send(Event::Tick).unwrap();
+
// return if there's no chunk at the player's position
{
- let dimension_lock = client.dimension.lock().unwrap();
- let player_lock = client.player.lock().unwrap();
+ let dimension_lock = client.dimension.lock();
+ let player_lock = client.player.lock();
let player_entity = player_lock.entity(&dimension_lock);
let player_entity = if let Some(player_entity) = player_entity {
player_entity
@@ -749,8 +752,27 @@ impl Client {
client.ai_step();
// TODO: minecraft does ambient sounds here
+ }
- tx.send(Event::GameTick).unwrap();
+ /// Returns the entity associated to the player.
+ pub fn entity_mut<'d>(&self, dimension: &'d mut Dimension) -> EntityMut<'d> {
+ let entity_id = {
+ let player_lock = self.player.lock();
+ player_lock.entity_id
+ };
+ dimension
+ .entity_mut(entity_id)
+ .expect("Player entity should be in the given dimension")
+ }
+ /// Returns the entity associated to the player.
+ pub fn entity<'d>(&self, dimension: &'d Dimension) -> EntityRef<'d> {
+ let entity_id = {
+ let player_lock = self.player.lock();
+ player_lock.entity_id
+ };
+ dimension
+ .entity(entity_id)
+ .expect("Player entity should be in the given dimension")
}
}
diff --git a/azalea-client/src/movement.rs b/azalea-client/src/movement.rs
index ab324370..fb4a5968 100644
--- a/azalea-client/src/movement.rs
+++ b/azalea-client/src/movement.rs
@@ -31,9 +31,9 @@ impl Client {
/// This gets called every tick.
pub async fn send_position(&mut self) -> Result<(), MovePlayerError> {
let packet = {
- let player_lock = self.player.lock().unwrap();
- let mut physics_state = self.physics_state.lock().unwrap();
- let mut dimension_lock = self.dimension.lock().unwrap();
+ let player_lock = self.player.lock();
+ let mut physics_state = self.physics_state.lock();
+ let mut dimension_lock = self.dimension.lock();
let mut player_entity = player_lock
.entity_mut(&mut dimension_lock)
@@ -129,8 +129,8 @@ impl Client {
// Set our current position to the provided Vec3, potentially clipping through blocks.
pub async fn set_pos(&mut self, new_pos: Vec3) -> Result<(), MovePlayerError> {
- let player_lock = self.player.lock().unwrap();
- let mut dimension_lock = self.dimension.lock().unwrap();
+ let player_lock = self.player.lock();
+ let mut dimension_lock = self.dimension.lock();
dimension_lock.set_entity_pos(player_lock.entity_id, new_pos)?;
@@ -138,8 +138,8 @@ impl Client {
}
pub async fn move_entity(&mut self, movement: &Vec3) -> Result<(), MovePlayerError> {
- let mut dimension_lock = self.dimension.lock().unwrap();
- let player = self.player.lock().unwrap();
+ let mut dimension_lock = self.dimension.lock();
+ let player = self.player.lock();
let mut entity = player
.entity_mut(&mut dimension_lock)
@@ -157,25 +157,17 @@ impl Client {
pub fn ai_step(&mut self) {
self.tick_controls(None);
- let player_lock = self.player.lock().unwrap();
- let mut dimension_lock = self.dimension.lock().unwrap();
-
+ let player_lock = self.player.lock();
+ let mut dimension_lock = self.dimension.lock();
let mut player_entity = player_lock
.entity_mut(&mut dimension_lock)
.expect("Player must exist");
// server ai step
{
- let mut physics_state = self.physics_state.lock().unwrap();
+ let physics_state = self.physics_state.lock();
player_entity.xxa = physics_state.left_impulse;
player_entity.zza = physics_state.forward_impulse;
-
- // handle jumping_once
- if physics_state.jumping_once {
- player_entity.jumping = true;
- } else if player_entity.jumping {
- physics_state.jumping_once = false;
- }
}
player_entity.ai_step();
@@ -183,7 +175,7 @@ impl Client {
/// Update the impulse from self.move_direction. The multipler is used for sneaking.
pub(crate) fn tick_controls(&mut self, multiplier: Option<f32>) {
- let mut physics_state = self.physics_state.lock().unwrap();
+ let mut physics_state = self.physics_state.lock();
let mut forward_impulse: f32 = 0.;
let mut left_impulse: f32 = 0.;
@@ -219,31 +211,29 @@ impl Client {
/// Start walking in the given direction.
pub fn walk(&mut self, direction: MoveDirection) {
- let mut physics_state = self.physics_state.lock().unwrap();
+ let mut physics_state = self.physics_state.lock();
physics_state.move_direction = direction;
}
- /// Jump once next tick. This acts as if you pressed space for one tick in
- /// vanilla. If you want to jump continuously, use `set_jumping`.
- pub fn jump(&mut self) {
- let mut physics_state = self.physics_state.lock().unwrap();
- physics_state.jumping_once = true;
- }
-
/// Toggle whether we're jumping. This acts as if you held space in
/// vanilla. If you want to jump once, use the `jump` function.
///
/// If you're making a realistic client, calling this function every tick is
/// recommended.
pub fn set_jumping(&mut self, jumping: bool) {
- let player_lock = self.player.lock().unwrap();
- let mut dimension_lock = self.dimension.lock().unwrap();
- let mut player_entity = player_lock
- .entity_mut(&mut dimension_lock)
- .expect("Player must exist");
+ let mut dimension = self.dimension.lock();
+ let mut player_entity = self.entity_mut(&mut dimension);
player_entity.jumping = jumping;
}
+
+ /// Returns whether the player will try to jump next tick.
+ pub fn jumping(&self) -> bool {
+ let dimension = self.dimension.lock();
+ let player_entity = self.entity(&dimension);
+
+ player_entity.jumping
+ }
}
#[derive(Clone, Copy, Debug, Default)]
diff --git a/azalea/Cargo.toml b/azalea/Cargo.toml
index 77f07ab3..29f45ed6 100644
--- a/azalea/Cargo.toml
+++ b/azalea/Cargo.toml
@@ -9,7 +9,13 @@ version = "0.1.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+anyhow = "^1.0.65"
+async-trait = "^0.1.57"
azalea-client = {version = "0.1.0", path = "../azalea-client"}
+azalea-protocol = {version = "0.1.0", path = "../azalea-protocol"}
+parking_lot = "^0.12.1"
+thiserror = "^1.0.37"
+tokio = "^1.21.1"
[dev-dependencies]
anyhow = "^1.0.65"
diff --git a/azalea/README.md b/azalea/README.md
index 6678052c..2ea99de0 100644
--- a/azalea/README.md
+++ b/azalea/README.md
@@ -1 +1,3 @@
-A wrapper over azalea-client, adding useful functions for making bots.
+A framework for creating Minecraft bots.
+
+Interally, it's just a wrapper over azalea-client, adding useful functions for making bots.
diff --git a/azalea/examples/craft_dig_straight_down.rs b/azalea/examples/craft_dig_straight_down.rs
index 53e5cae8..3b7267ef 100644
--- a/azalea/examples/craft_dig_straight_down.rs
+++ b/azalea/examples/craft_dig_straight_down.rs
@@ -1,45 +1,66 @@
-use azalea::{Bot, Event};
+use azalea::{pathfinder, Account};
+use azalea::{Bot, Client, Event};
+use parking_lot::Mutex;
+use std::sync::Arc;
-struct Context {
- pub started: bool
+#[derive(Default)]
+struct State {
+ pub started: bool,
}
#[tokio::main]
async fn main() {
- let bot = Bot::offline("bot");
+ let account = Account::offline("bot");
// or let bot = azalea::Bot::microsoft("access token").await;
- bot.join("localhost".try_into().unwrap()).await.unwrap();
-
- let ctx = Arc::new(Mutex::new(Context { started: false }));
-
- loop {
- tokio::spawn(handle_event(bot.next().await, bot, ctx.clone()));
- }
+ azalea::start(azalea::Options {
+ account,
+ address: "localhost",
+ state: Arc::new(Mutex::new(State::default())),
+ plugins: vec![],
+ handle,
+ })
+ .await
+ .unwrap();
}
-
-async fn handle_event(event: &Event, bot: &Bot, ctx: Arc<Context>) {
+async fn handle(bot: Client, event: Arc<Event>, state: Arc<Mutex<State>>) {
match event {
- Event::Message(m) {
- if m.username == bot.player.username { return };
+ Event::Message(m) => {
+ if m.username == bot.player.username {
+ return;
+ };
if m.message = "go" {
// make sure we only start once
let ctx_lock = ctx.lock().unwrap();
- if ctx_lock.started { return };
+ if ctx_lock.started {
+ return;
+ };
ctx_lock.started = true;
drop(ctx_lock);
- bot.goto(
- pathfinder::Goals::NearXZ(5, azalea::BlockXZ(0, 0))
- ).await;
- let chest = bot.open_container(&bot.world.find_one_block(|b| b.id == "minecraft:chest")).await.unwrap();
- bot.take_amount(&chest, 5, |i| i.id == "#minecraft:planks").await;
+ bot.goto(pathfinder::Goals::NearXZ(5, azalea::BlockXZ(0, 0)))
+ .await;
+ let chest = bot
+ .open_container(&bot.world.find_one_block(|b| b.id == "minecraft:chest"))
+ .await
+ .unwrap();
+ bot.take_amount(&chest, 5, |i| i.id == "#minecraft:planks")
+ .await;
chest.close().await;
- let crafting_table = bot.open_crafting_table(&bot.world.find_one_block(|b| b.id == "minecraft:crafting_table")).await.unwrap();
- bot.craft(&crafting_table, &bot.recipe_for("minecraft:sticks")).await?;
- let pickaxe = bot.craft(&crafting_table, &bot.recipe_for("minecraft:wooden_pickaxe")).await?;
+ let crafting_table = bot
+ .open_crafting_table(
+ &bot.world
+ .find_one_block(|b| b.id == "minecraft:crafting_table"),
+ )
+ .await
+ .unwrap();
+ bot.craft(&crafting_table, &bot.recipe_for("minecraft:sticks"))
+ .await?;
+ let pickaxe = bot
+ .craft(&crafting_table, &bot.recipe_for("minecraft:wooden_pickaxe"))
+ .await?;
crafting_table.close().await;
bot.hold(&pickaxe);
@@ -50,7 +71,7 @@ async fn handle_event(event: &Event, bot: &Bot, ctx: Arc<Context>) {
}
}
}
- },
+ }
_ => {}
}
-} \ No newline at end of file
+}
diff --git a/azalea/examples/echo.rs b/azalea/examples/echo.rs
index c9e46a09..75cd235f 100644
--- a/azalea/examples/echo.rs
+++ b/azalea/examples/echo.rs
@@ -1,38 +1,46 @@
-use azalea::{Account, Event};
+use std::sync::Arc;
-let account = Account::offline("bot");
-// or let account = azalea::Account::microsoft("access token").await;
+use azalea::{Account, Client, Event};
+use parking_lot::Mutex;
-let bot = account.join("localhost".try_into().unwrap()).await.unwrap();
+#[tokio::main]
+async fn main() {
+ let account = Account::offline("bot");
+ // or let account = azalea::Account::microsoft("access token").await;
-loop {
- match bot.next().await {
- Event::Message(m) {
- if m.username == bot.username { return };
+ azalea::start(azalea::Options {
+ account,
+ address: "localhost",
+ state: Arc::new(Mutex::new(State::default())),
+ plugins: vec![],
+ handle,
+ })
+ .await
+ .unwrap();
+}
+
+pub struct State {}
+
+async fn handle(bot: Client, event: Arc<Event>, state: Arc<Mutex<State>>) -> anyhow::Result<()> {
+ match event {
+ Event::Chat(m) => {
+ if m.username == bot.username {
+ return Ok(()); // ignore our own messages
+ };
bot.chat(m.message).await;
- },
- Event::Kicked(m) {
+ }
+ Event::Kick(m) => {
println!(m);
bot.reconnect().await.unwrap();
- },
- Event::Hunger(h) {
+ }
+ Event::HungerUpdate(h) => {
if !h.using_held_item() && h.hunger <= 17 {
- match bot.hold(azalea::ItemGroup::Food).await {
- Ok(_) => {},
- Err(e) => {
- println!("{}", e);
- break;
- }
- }
- match bot.use_held_item().await {
- Ok(_) => {},
- Err(e) => {
- println!("{}", e);
- break;
- }
- }
+ bot.hold(azalea::ItemGroup::Food).await?;
+ bot.use_held_item().await?;
}
}
_ => {}
}
+
+ Ok(())
}
diff --git a/azalea/examples/mine_a_chunk.rs b/azalea/examples/mine_a_chunk.rs
index 6549f2b2..bc576513 100644
--- a/azalea/examples/mine_a_chunk.rs
+++ b/azalea/examples/mine_a_chunk.rs
@@ -1,6 +1,6 @@
-use azalea::{Account, Accounts, Event, pathfinder};
-
-// You can use the `azalea::Bots` struct to control many bots as one unit.
+use azalea::{pathfinder, Account, Accounts, Client, Event};
+use parking_lot::Mutex;
+use std::sync::Arc;
#[tokio::main]
async fn main() {
@@ -10,18 +10,60 @@ async fn main() {
accounts.add(Account::offline(format!("bot{}", i)));
}
- let bots = accounts.join("localhost".try_into().unwrap()).await.unwrap();
+ azalea::start_group(azalea::GroupOptions {
+ accounts,
+ address: "localhost",
+
+ group_state: Arc::new(Mutex::new(State::default())),
+ state: State::default(),
- bots.goto(azalea::BlockPos::new(0, 70, 0)).await;
- // or bots.goto_goal(pathfinder::Goals::Goto(azalea::BlockPos(0, 70, 0))).await;
+ group_plugins: vec![Arc::new(pathfinder::Plugin::default())],
+ plugins: vec![],
+
+ handle: Box::new(handle),
+ group_handle: Box::new(handle),
+ })
+ .await
+ .unwrap();
+}
- // destroy the blocks in this area and then leave
+#[derive(Default)]
+struct State {}
+
+#[derive(Default)]
+struct GroupState {}
+
+async fn handle(bot: Client, event: Arc<Event>, state: Arc<Mutex<State>>) -> anyhow::Result<()> {
+ match event {
+ _ => {}
+ }
+
+ Ok(())
+}
+
+async fn group_handle(
+ bots: Swarm,
+ event: Arc<Event>,
+ state: Arc<Mutex<GroupState>>,
+) -> anyhow::Result<()> {
+ match *event {
+ Event::Login => {
+ bots.goto(azalea::BlockPos::new(0, 70, 0)).await;
+ // or bots.goto_goal(pathfinder::Goals::Goto(azalea::BlockPos(0, 70, 0))).await;
+
+ // destroy the blocks in this area and then leave
+
+ bots.fill(
+ azalea::Selection::Range(
+ azalea::BlockPos::new(0, 0, 0),
+ azalea::BlockPos::new(16, 255, 16),
+ ),
+ azalea::block::Air,
+ )
+ .await;
+ }
+ _ => {}
+ }
- bots.fill(
- azalea::Selection::Range(
- azalea::BlockPos::new(0, 0, 0),
- azalea::BlockPos::new(16, 255, 16)
- ),
- azalea::block::Air
- ).await;
+ Ok(())
}
diff --git a/azalea/examples/potatobot/autoeat.rs b/azalea/examples/potatobot/autoeat.rs
index 44702295..d1296c29 100644
--- a/azalea/examples/potatobot/autoeat.rs
+++ b/azalea/examples/potatobot/autoeat.rs
@@ -1,20 +1,29 @@
//! Automatically eat when we get hungry.
+use async_trait::async_trait;
use azalea::{Client, Event};
use std::sync::{Arc, Mutex};
#[derive(Default)]
+pub struct Plugin {
+ pub state: Arc<Mutex<State>>,
+}
+
+#[derive(Default)]
pub struct State {}
-pub async fn handle(bot: &mut Client, event: Event, state: Arc<Mutex<State>>) {
- match event {
- Event::UpdateHunger => {
- if !bot.using_held_item() && bot.food_level() <= 17 {
- if bot.hold(azalea::ItemGroup::Food).await {
- bot.use_held_item().await;
+#[async_trait]
+impl azalea::Plugin for Plugin {
+ async fn handle(self: Arc<Self>, bot: Client, event: Arc<Event>) {
+ match event {
+ Event::UpdateHunger => {
+ if !bot.using_held_item() && bot.food_level() <= 17 {
+ if bot.hold(azalea::ItemGroup::Food).await {
+ bot.use_held_item().await;
+ }
}
}
+ _ => {}
}
- _ => {}
}
}
diff --git a/azalea/examples/potatobot/main.rs b/azalea/examples/potatobot/main.rs
index 94ed0005..a04b199d 100644
--- a/azalea/examples/potatobot/main.rs
+++ b/azalea/examples/potatobot/main.rs
@@ -1,49 +1,34 @@
mod autoeat;
-use azalea::{pathfinder, Account, BlockPos, Client, Event, ItemKind, MoveDirection, Vec3};
-use std::{
- convert::TryInto,
- sync::{Arc, Mutex},
-};
+use azalea::prelude::*;
+use azalea::{pathfinder, Account, BlockPos, Client, Event, ItemKind, MoveDirection, Plugin, Vec3};
+use parking_lot::Mutex;
+use std::sync::Arc;
#[derive(Default)]
-struct State {
- pub eating: bool,
-}
+struct State {}
#[tokio::main]
async fn main() {
env_logger::init();
let account = Account::offline("bot");
- let (bot, mut rx) = account
- .join(&"localhost".try_into().unwrap())
- .await
- .unwrap();
- // Maybe all this could be turned into a macro in the future?
- let state = Arc::new(Mutex::new(State::default()));
- let autoeat_state = Arc::new(Mutex::new(autoeat::State::default()));
- let pathfinder_state = Arc::new(Mutex::new(pathfinder::State::default()));
- while let Some(event) = rx.recv().await {
- // we put it into an Arc so it's cheaper to clone
- let event = Arc::new(event);
-
- tokio::spawn(autoeat::handle(
- bot.clone(),
- event.clone(),
- autoeat_state.clone(),
- ));
- tokio::spawn(pathfinder::handle(
- bot.clone(),
- event.clone(),
- pathfinder_state.clone(),
- ));
- tokio::spawn(handle(bot.clone(), event.clone(), state.clone()));
- }
+ azalea::start(azalea::Options {
+ account,
+ address: "localhost",
+ state: Arc::new(Mutex::new(State::default())),
+ plugins: vec![
+ Arc::new(autoeat::Plugin::default()),
+ Arc::new(pathfinder::Plugin::default()),
+ ],
+ handle,
+ })
+ .await
+ .unwrap();
}
-async fn handle(bot: Client, event: Event, state: Arc<Mutex<State>>) -> anyhow::Result<()> {
+async fn handle(bot: Client, event: Arc<Event>, state: Arc<Mutex<State>>) -> anyhow::Result<()> {
match event {
Event::Login => {
goto_farm(bot, state).await?;
@@ -58,8 +43,7 @@ async fn handle(bot: Client, event: Event, state: Arc<Mutex<State>>) -> anyhow::
// go to the place where we start farming
async fn goto_farm(bot: Client, state: Arc<Mutex<State>>) -> anyhow::Result<()> {
- bot.state
- .goto(pathfinder::Goals::Near(5, BlockPos::new(0, 70, 0)))
+ bot.goto(pathfinder::Goals::Near(5, BlockPos::new(0, 70, 0)))
.await?;
Ok(())
}
@@ -69,7 +53,7 @@ async fn deposit(bot: &mut Client, state: &mut Arc<Mutex<State>>) -> anyhow::Res
// first throw away any garbage we might have
bot.toss(|item| item.kind != ItemKind::Potato && item.kind != ItemKind::DiamondHoe);
- bot.state.goto(Vec3::new(0, 70, 0)).await?;
+ bot.goto(Vec3::new(0, 70, 0)).await?;
let chest = bot
.open_container(&bot.dimension.block_at(BlockPos::new(0, 70, 0)))
.await
diff --git a/azalea/examples/pvp.rs b/azalea/examples/pvp.rs
index 5febdd45..9405cb6f 100644
--- a/azalea/examples/pvp.rs
+++ b/azalea/examples/pvp.rs
@@ -1,22 +1,46 @@
-use azalea::{Account, Accounts, Event, pathfinder};
+use std::sync::Arc;
+
+use azalea::{pathfinder, Account, Accounts, Client, Event};
+use parking_lot::Mutex;
#[tokio::main]
async fn main() {
let accounts = Accounts::new();
+
for i in 0..10 {
accounts.add(Account::offline(format!("bot{}", i)));
}
- let bots = accounts.join("localhost".try_into().unwrap()).await.unwrap();
+ azalea::start_swarm(azalea::SwarmOptions {
+ accounts,
+ address: "localhost",
+
+ swarm_state: Arc::new(Mutex::new(State::default())),
+ state: State::default(),
+
+ swarm_plugins: vec![Arc::new(pathfinder::Plugin::default())],
+ plugins: vec![],
+
+ handle: Box::new(handle),
+ swarm_handle: Box::new(handle),
+ })
+ .await
+ .unwrap();
+}
+
+struct State {}
+struct SwarmState {}
- match bots.next().await {
- Event::Tick {
+async fn handle(bots: Client, event: Arc<Event>, state: Arc<Mutex<State>>) {
+ match *event {
+ Event::Tick => {
// choose an arbitrary player within render distance to target
- if let Some(target) = bots.world.find_one_entity(|e| e.id == "minecraft:player") {
+ if let Some(target) = bots
+ .dimension()
+ .find_one_entity(|e| e.id == "minecraft:player")
+ {
for bot in bots {
- bot.tick_goto_goal(
- pathfinder::Goals::Reach(target.bounding_box)
- );
+ bot.tick_goto_goal(pathfinder::Goals::Reach(target.bounding_box));
// if target.bounding_box.distance(bot.eyes) < bot.reach_distance() {
if bot.entity.can_reach(target.bounding_box) {
bot.swing();
@@ -27,7 +51,7 @@ async fn main() {
}
}
}
- },
+ }
_ => {}
}
}
diff --git a/azalea/src/bot.rs b/azalea/src/bot.rs
index 6746e09e..a77e2a1c 100644
--- a/azalea/src/bot.rs
+++ b/azalea/src/bot.rs
@@ -1,14 +1,46 @@
-pub struct BotState {
+use crate::{Client, Event};
+use async_trait::async_trait;
+use parking_lot::Mutex;
+use std::sync::Arc;
+
+#[derive(Default)]
+pub struct Plugin {
+ pub state: Arc<Mutex<State>>,
+}
+
+#[derive(Default)]
+pub struct State {
jumping_once: bool,
}
pub trait BotTrait {
- fn jump(&mut self);
+ fn jump(&self);
}
impl BotTrait for azalea_client::Client {
- fn jump(&mut self) {
- let mut physics_state = self.physics_state.lock().unwrap();
- physics_state.jumping_once = true;
+ /// Try to jump next tick.
+ fn jump(&self) {
+ let player_lock = self.player.lock();
+ let mut dimension_lock = self.dimension.lock();
+
+ let mut player_entity = player_lock
+ .entity_mut(&mut dimension_lock)
+ .expect("Player must exist");
+
+ player_entity.jumping = true;
+ }
+}
+
+#[async_trait]
+impl crate::Plugin for Plugin {
+ async fn handle(self: Arc<Self>, mut bot: Client, event: Arc<Event>) {
+ if let Event::Tick = *event {
+ let mut state = self.state.lock();
+ if bot.jumping() {
+ state.jumping_once = false;
+ } else if state.jumping_once {
+ bot.set_jumping(true);
+ }
+ }
}
}
diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs
index fe8a3740..8ef02e7c 100644
--- a/azalea/src/lib.rs
+++ b/azalea/src/lib.rs
@@ -1,4 +1,84 @@
mod bot;
pub mod prelude;
-pub use azalea_client::Client;
+use async_trait::async_trait;
+pub use azalea_client::*;
+use azalea_protocol::ServerAddress;
+use parking_lot::Mutex;
+use std::{future::Future, sync::Arc};
+use thiserror::Error;
+
+/// Plugins can keep their own personal state, listen to events, and add new functions to Client.
+#[async_trait]
+pub trait Plugin: Send + Sync {
+ async fn handle(self: Arc<Self>, bot: Client, event: Arc<Event>);
+}
+
+// pub type HeuristicFn<N, W> = fn(start: &Vertex<N, W>, current: &Vertex<N, W>) -> W;
+pub type HandleFn<Fut, S> = fn(Client, Arc<Event>, Arc<Mutex<S>>) -> Fut;
+
+pub struct Options<S, A, Fut>
+where
+ A: TryInto<ServerAddress>,
+ Fut: Future<Output = Result<(), anyhow::Error>>,
+{
+ pub address: A,
+ pub account: Account,
+ pub plugins: Vec<Arc<dyn Plugin>>,
+ pub state: Arc<Mutex<S>>,
+ pub handle: HandleFn<Fut, S>,
+}
+
+#[derive(Error, Debug)]
+pub enum Error {
+ #[error("Invalid address")]
+ InvalidAddress,
+}
+
+/// Join a Minecraft server.
+///
+/// ```no_run
+/// azalea::start(azalea::Options {
+/// account,
+/// address: "localhost",
+/// state: Arc::new(Mutex::new(State::default())),
+/// plugins: vec![&autoeat::Plugin::default()],
+/// handle: Box::new(handle),
+/// }).await.unwrap();
+/// ```
+pub async fn start<
+ S: Send + 'static,
+ A: Send + TryInto<ServerAddress>,
+ Fut: Future<Output = Result<(), anyhow::Error>> + Send + 'static,
+>(
+ options: Options<S, A, Fut>,
+) -> Result<(), Error> {
+ let address = match options.address.try_into() {
+ Ok(address) => address,
+ Err(_) => return Err(Error::InvalidAddress),
+ };
+
+ let (bot, mut rx) = options.account.join(&address).await.unwrap();
+
+ let state = options.state;
+ let bot_plugin = Arc::new(bot::Plugin::default());
+
+ while let Some(event) = rx.recv().await {
+ // we put it into an Arc so it's cheaper to clone
+ let event = Arc::new(event);
+
+ for plugin in &options.plugins {
+ tokio::spawn(plugin.clone().handle(bot.clone(), event.clone()));
+ }
+
+ {
+ let bot_plugin = bot_plugin.clone();
+ let bot = bot.clone();
+ let event = event.clone();
+ tokio::spawn(bot::Plugin::handle(bot_plugin, bot, event));
+ };
+ tokio::spawn((options.handle)(bot.clone(), event.clone(), state.clone()));
+ }
+
+ Ok(())
+}
diff --git a/bot/Cargo.toml b/bot/Cargo.toml
index b51e6705..53f8637b 100755
--- a/bot/Cargo.toml
+++ b/bot/Cargo.toml
@@ -7,10 +7,8 @@ version = "0.1.0"
[dependencies]
anyhow = "1.0.65"
-azalea-client = {path = "../azalea-client"}
-azalea-core = {path = "../azalea-core"}
-azalea-physics = {path = "../azalea-physics"}
-azalea-protocol = {path = "../azalea-protocol"}
+azalea = { path = "../azalea" }
env_logger = "0.9.1"
tokio = "1.19.2"
uuid = "1.1.2"
+parking_lot = "^0.12.1"
diff --git a/bot/src/main.rs b/bot/src/main.rs
index 9b2eea1f..0a291fd8 100644
--- a/bot/src/main.rs
+++ b/bot/src/main.rs
@@ -1,35 +1,31 @@
-use azalea_client::{Account, Client, Event, MoveDirection};
-use std::convert::TryInto;
+use azalea::prelude::*;
+use azalea::{Account, Client, Event};
+use parking_lot::Mutex;
+use std::sync::Arc;
+
+#[derive(Default)]
+struct State {}
#[tokio::main]
async fn main() {
env_logger::init();
- let bot = Account::offline("bot");
-
- let (bot, mut rx) = bot.join(&"localhost".try_into().unwrap()).await.unwrap();
+ let account = Account::offline("bot");
- while let Some(event) = rx.recv().await {
- tokio::spawn(handle_event(event, bot.clone()));
- }
+ azalea::start(azalea::Options {
+ account,
+ address: "localhost",
+ state: Arc::new(Mutex::new(State::default())),
+ plugins: vec![],
+ handle,
+ })
+ .await
+ .unwrap();
}
-async fn handle_event(event: Event, mut bot: Client) -> anyhow::Result<()> {
- match event {
- Event::Login => {
- // tokio::time::sleep(std::time::Duration::from_secs(1)).await;
- // bot.walk(MoveDirection::Forward);
-
- // loop {
- // tokio::time::sleep(std::time::Duration::from_secs(2)).await;
- // }
- // bot.walk(MoveDirection::None);
- }
- Event::GameTick => {
- bot.set_jumping(true);
- }
- Event::Packet(_packet) => {}
- _ => {}
+async fn handle(bot: Client, event: Arc<Event>, _state: Arc<Mutex<State>>) -> anyhow::Result<()> {
+ if let Event::Tick = *event {
+ bot.jump();
}
Ok(())