diff options
| author | mat <27899617+mat-1@users.noreply.github.com> | 2022-10-07 19:57:42 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-10-07 19:57:42 -0500 |
| commit | ba4cfaafaec97a3c5b9405fe542035ebe9039edd (patch) | |
| tree | 5fc7340a49f96d84f86ed6adf400ad461c47d1b6 /azalea | |
| parent | e0bcab53b8a3721a008e47062c6b5972fa64b8ad (diff) | |
| download | azalea-drasl-ba4cfaafaec97a3c5b9405fe542035ebe9039edd.tar.xz | |
Bot API (#27)
Basically make the `azalea` crate have stuff
Diffstat (limited to 'azalea')
| -rw-r--r-- | azalea/Cargo.toml | 6 | ||||
| -rw-r--r-- | azalea/README.md | 4 | ||||
| -rw-r--r-- | azalea/examples/craft_dig_straight_down.rs | 73 | ||||
| -rw-r--r-- | azalea/examples/echo.rs | 60 | ||||
| -rw-r--r-- | azalea/examples/mine_a_chunk.rs | 70 | ||||
| -rw-r--r-- | azalea/examples/potatobot/autoeat.rs | 23 | ||||
| -rw-r--r-- | azalea/examples/potatobot/main.rs | 56 | ||||
| -rw-r--r-- | azalea/examples/pvp.rs | 42 | ||||
| -rw-r--r-- | azalea/src/bot.rs | 42 | ||||
| -rw-r--r-- | azalea/src/lib.rs | 82 |
10 files changed, 333 insertions, 125 deletions
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(()) +} |
