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]] @@ -1046,6 +1059,19 @@ dependencies = [ "winapi", ] +[[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" @@ -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), } @@ -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) { + 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) { - 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) { +async fn handle(bot: Client, event: Arc, state: Arc>) { 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) { } } } - }, + } _ => {} } -} \ 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, state: Arc>) -> 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", - bots.goto(azalea::BlockPos::new(0, 70, 0)).await; - // or bots.goto_goal(pathfinder::Goals::Goto(azalea::BlockPos(0, 70, 0))).await; + group_state: Arc::new(Mutex::new(State::default())), + state: State::default(), - // destroy the blocks in this area and then leave + group_plugins: vec![Arc::new(pathfinder::Plugin::default())], + plugins: vec![], - bots.fill( - azalea::Selection::Range( - azalea::BlockPos::new(0, 0, 0), - azalea::BlockPos::new(16, 255, 16) - ), - azalea::block::Air - ).await; + handle: Box::new(handle), + group_handle: Box::new(handle), + }) + .await + .unwrap(); +} + +#[derive(Default)] +struct State {} + +#[derive(Default)] +struct GroupState {} + +async fn handle(bot: Client, event: Arc, state: Arc>) -> anyhow::Result<()> { + match event { + _ => {} + } + + Ok(()) +} + +async fn group_handle( + bots: Swarm, + event: Arc, + state: Arc>, +) -> 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; + } + _ => {} + } + + 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>, +} + #[derive(Default)] pub struct State {} -pub async fn handle(bot: &mut Client, event: Event, state: Arc>) { - 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, bot: Client, event: Arc) { + 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>) -> anyhow::Result<()> { +async fn handle(bot: Client, event: Arc, state: Arc>) -> anyhow::Result<()> { match event { Event::Login => { goto_farm(bot, state).await?; @@ -58,8 +43,7 @@ async fn handle(bot: Client, event: Event, state: Arc>) -> anyhow:: // go to the place where we start farming async fn goto_farm(bot: Client, state: Arc>) -> 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>) -> 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", - match bots.next().await { - Event::Tick { + 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 {} + +async fn handle(bots: Client, event: Arc, state: Arc>) { + 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>, +} + +#[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, mut bot: Client, event: Arc) { + 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, bot: Client, event: Arc); +} + +// pub type HeuristicFn = fn(start: &Vertex, current: &Vertex) -> W; +pub type HandleFn = fn(Client, Arc, Arc>) -> Fut; + +pub struct Options +where + A: TryInto, + Fut: Future>, +{ + pub address: A, + pub account: Account, + pub plugins: Vec>, + pub state: Arc>, + pub handle: HandleFn, +} + +#[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, + Fut: Future> + Send + 'static, +>( + options: Options, +) -> 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 account = Account::offline("bot"); - let (bot, mut rx) = bot.join(&"localhost".try_into().unwrap()).await.unwrap(); - - 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, _state: Arc>) -> anyhow::Result<()> { + if let Event::Tick = *event { + bot.jump(); } Ok(())