mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 14:26:04 +00:00
swarms
This commit is contained in:
parent
04c8d8db9f
commit
9b07178a06
9 changed files with 215 additions and 49 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -121,6 +121,7 @@ dependencies = [
|
|||
"azalea-world",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"nohash-hasher",
|
||||
"num-traits",
|
||||
"parking_lot",
|
||||
"priority-queue",
|
||||
|
|
|
@ -10,7 +10,7 @@ version = "0.3.0"
|
|||
|
||||
[dependencies]
|
||||
anyhow = "^1.0.65"
|
||||
async-trait = "^0.1.57"
|
||||
async-trait = "0.1.58"
|
||||
azalea-block = {version = "0.3.0", path = "../azalea-block"}
|
||||
azalea-client = {version = "0.3.0", path = "../azalea-client"}
|
||||
azalea-core = {version = "0.3.0", path = "../azalea-core"}
|
||||
|
@ -18,6 +18,7 @@ azalea-physics = {version = "0.3.0", path = "../azalea-physics"}
|
|||
azalea-protocol = {version = "0.3.0", path = "../azalea-protocol"}
|
||||
azalea-world = {version = "0.3.0", path = "../azalea-world"}
|
||||
futures = "0.3.25"
|
||||
nohash-hasher = "0.2.0"
|
||||
num-traits = "0.2.15"
|
||||
parking_lot = {version = "^0.12.1", features = ["deadlock_detection"]}
|
||||
priority-queue = "1.3.0"
|
||||
|
|
|
@ -10,6 +10,7 @@ async fn main() {
|
|||
|
||||
for i in 0..10 {
|
||||
accounts.push(Account::offline(&format!("bot{}", i)));
|
||||
states.push(Arc::new(Mutex::new(State::default())));
|
||||
}
|
||||
|
||||
azalea::start_swarm(azalea::SwarmOptions {
|
||||
|
|
|
@ -75,6 +75,9 @@
|
|||
//!
|
||||
//! [`azalea_client`]: https://crates.io/crates/azalea-client
|
||||
|
||||
#![feature(trait_upcasting)]
|
||||
#![allow(incomplete_features)]
|
||||
|
||||
mod bot;
|
||||
pub mod pathfinder;
|
||||
pub mod prelude;
|
||||
|
@ -87,22 +90,3 @@ pub use start::{start, Options};
|
|||
pub use swarm::*;
|
||||
|
||||
pub type HandleFn<Fut, S> = fn(Client, Event, S) -> Fut;
|
||||
|
||||
/// A helper macro that generates a [`Plugins`] struct from a list of objects
|
||||
/// that implement [`Plugin`].
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// plugins![azalea_pathfinder::Plugin::default()];
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! plugins {
|
||||
($($plugin:expr),*) => {
|
||||
{
|
||||
let mut plugins = azalea::Plugins::new();
|
||||
$(
|
||||
plugins.add($plugin);
|
||||
)*
|
||||
plugins
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
|
||||
pub use crate::bot::BotTrait;
|
||||
pub use crate::pathfinder::Trait;
|
||||
pub use crate::plugins;
|
||||
pub use crate::{plugins, swarm_plugins};
|
||||
pub use azalea_client::{Account, Client, Event};
|
||||
|
|
|
@ -4,6 +4,25 @@ use azalea_protocol::ServerAddress;
|
|||
use std::{future::Future, sync::Arc};
|
||||
use thiserror::Error;
|
||||
|
||||
/// A helper macro that generates a [`Plugins`] struct from a list of objects
|
||||
/// that implement [`Plugin`].
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// plugins![azalea_pathfinder::Plugin::default()];
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! plugins {
|
||||
($($plugin:expr),*) => {
|
||||
{
|
||||
let mut plugins = azalea::Plugins::new();
|
||||
$(
|
||||
plugins.add($plugin);
|
||||
)*
|
||||
plugins
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// The options that are passed to [`azalea::start`].
|
||||
///
|
||||
/// [`azalea::start`]: crate::start
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
mod plugins;
|
||||
|
||||
pub use self::plugins::*;
|
||||
use crate::{bot, HandleFn};
|
||||
use async_trait::async_trait;
|
||||
use azalea_client::{Account, Client, Event, JoinError, Plugin, Plugins};
|
||||
use azalea_protocol::{
|
||||
resolver::{self, ResolverError},
|
||||
|
@ -9,10 +13,29 @@ use futures::{
|
|||
FutureExt,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use std::{future::Future, sync::Arc};
|
||||
use std::{any::Any, future::Future, sync::Arc};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
|
||||
/// A helper macro that generates a [`Plugins`] struct from a list of objects
|
||||
/// that implement [`Plugin`].
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// plugins![azalea_pathfinder::Plugin::default()];
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! swarm_plugins {
|
||||
($($plugin:expr),*) => {
|
||||
{
|
||||
let mut plugins = azalea::SwarmPlugins::new();
|
||||
$(
|
||||
plugins.add($plugin);
|
||||
)*
|
||||
plugins
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// A swarm is a way to conveniently control many bots at once, while also
|
||||
/// being able to control bots at an individual level when desired.
|
||||
#[derive(Clone)]
|
||||
|
@ -28,13 +51,16 @@ pub enum SwarmEvent {
|
|||
Login,
|
||||
}
|
||||
|
||||
pub type SwarmHandleFn<Fut, S> = fn(Swarm, SwarmEvent, S) -> Fut;
|
||||
|
||||
/// The options that are passed to [`azalea::start_swarm`].
|
||||
///
|
||||
/// [`azalea::start`]: crate::start_swarm
|
||||
pub struct SwarmOptions<S, SS, A, Fut>
|
||||
pub struct SwarmOptions<S, SS, A, Fut, SwarmFut>
|
||||
where
|
||||
A: TryInto<ServerAddress>,
|
||||
Fut: Future<Output = Result<(), anyhow::Error>>,
|
||||
SwarmFut: Future<Output = Result<(), anyhow::Error>>,
|
||||
{
|
||||
/// The address of the server that we're connecting to. This can be a
|
||||
/// `&str`, [`ServerAddress`], or anything that implements
|
||||
|
@ -45,13 +71,13 @@ where
|
|||
/// The accounts that are going to join the server.
|
||||
pub accounts: Vec<Account>,
|
||||
pub plugins: Plugins,
|
||||
pub swarm_plugins: Plugins,
|
||||
pub swarm_plugins: SwarmPlugins,
|
||||
/// The individual bot states. This must be the same length as `accounts`,
|
||||
/// since each bot gets one state.
|
||||
pub states: Vec<S>,
|
||||
pub swarm_state: SS,
|
||||
pub handle: HandleFn<Fut, S>,
|
||||
pub swarm_handle: HandleFn<Fut, S>,
|
||||
pub swarm_handle: SwarmHandleFn<SwarmFut, SS>,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
@ -70,8 +96,9 @@ pub async fn start_swarm<
|
|||
SS: Send + Sync + Clone + 'static,
|
||||
A: Send + TryInto<ServerAddress>,
|
||||
Fut: Future<Output = Result<(), anyhow::Error>> + Send + 'static,
|
||||
SwarmFut: Future<Output = Result<(), anyhow::Error>> + Send + 'static,
|
||||
>(
|
||||
options: SwarmOptions<S, SS, A, Fut>,
|
||||
options: SwarmOptions<S, SS, A, Fut, SwarmFut>,
|
||||
) -> Result<(), SwarmStartError> {
|
||||
assert_eq!(
|
||||
options.accounts.len(),
|
||||
|
@ -116,6 +143,21 @@ pub async fn start_swarm<
|
|||
|
||||
let states = options.states;
|
||||
let swarm_state = options.swarm_state;
|
||||
let mut internal_state = InternalSwarmState::default();
|
||||
|
||||
// Send an event to the swarm_handle function.
|
||||
let cloned_swarm = swarm.clone();
|
||||
let fire_swarm_event = move |event: SwarmEvent| {
|
||||
let cloned_swarm_plugins = options.swarm_plugins.clone();
|
||||
for plugin in cloned_swarm_plugins.into_iter() {
|
||||
tokio::spawn(plugin.handle(event.clone(), cloned_swarm.clone()));
|
||||
}
|
||||
tokio::spawn((options.swarm_handle)(
|
||||
cloned_swarm.clone(),
|
||||
event,
|
||||
swarm_state.clone(),
|
||||
));
|
||||
};
|
||||
|
||||
// bot events
|
||||
while let (Some(event), bot_index) = swarm.bot_recv().await {
|
||||
|
@ -132,6 +174,18 @@ pub async fn start_swarm<
|
|||
event.clone(),
|
||||
bot.clone(),
|
||||
));
|
||||
|
||||
// swarm event handling
|
||||
match &event {
|
||||
Event::Login => {
|
||||
internal_state.clients_joined += 1;
|
||||
if internal_state.clients_joined == swarm.bots.lock().len() {
|
||||
fire_swarm_event(SwarmEvent::Login);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
tokio::spawn((options.handle)(bot, event, bot_state));
|
||||
}
|
||||
|
||||
|
@ -151,3 +205,9 @@ impl Swarm {
|
|||
(event, index)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct InternalSwarmState {
|
||||
/// The number of clients connected to the server
|
||||
pub clients_joined: usize,
|
||||
}
|
78
azalea/src/swarm/plugins.rs
Normal file
78
azalea/src/swarm/plugins.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
use crate::{Client, Event, Swarm, SwarmEvent};
|
||||
use async_trait::async_trait;
|
||||
use nohash_hasher::NoHashHasher;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
collections::HashMap,
|
||||
hash::BuildHasherDefault,
|
||||
};
|
||||
|
||||
// kind of based on https://docs.rs/http/latest/src/http/extensions.rs.html
|
||||
/// A map of plugin ids to Plugin trait objects. The client stores this so we
|
||||
/// can keep the state for our plugins.
|
||||
///
|
||||
/// If you're using azalea, you should generate this from the `plugins!` macro.
|
||||
#[derive(Clone)]
|
||||
pub struct SwarmPlugins {
|
||||
map: Option<HashMap<TypeId, Box<dyn SwarmPlugin>, BuildHasherDefault<NoHashHasher<u64>>>>,
|
||||
}
|
||||
|
||||
impl SwarmPlugins {
|
||||
pub fn new() -> Self {
|
||||
Self { map: None }
|
||||
}
|
||||
|
||||
pub fn add<T: SwarmPlugin>(&mut self, plugin: T) {
|
||||
if self.map.is_none() {
|
||||
self.map = Some(HashMap::with_hasher(BuildHasherDefault::default()));
|
||||
}
|
||||
self.map
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.insert(TypeId::of::<T>(), Box::new(plugin));
|
||||
}
|
||||
|
||||
pub fn get<T: SwarmPlugin>(&self) -> Option<&T> {
|
||||
self.map
|
||||
.as_ref()
|
||||
.and_then(|map| map.get(&TypeId::of::<T>()))
|
||||
.and_then(|boxed| (boxed.as_ref() as &dyn Any).downcast_ref::<T>())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for SwarmPlugins {
|
||||
type Item = Box<dyn SwarmPlugin>;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.map
|
||||
.map(|map| map.into_iter().map(|(_, v)| v).collect::<Vec<_>>())
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Plugins can keep their own personal state, listen to events, and add new functions to Client.
|
||||
#[async_trait]
|
||||
pub trait SwarmPlugin: Send + Sync + SwarmPluginClone + Any + 'static {
|
||||
async fn handle(self: Box<Self>, event: SwarmEvent, swarm: Swarm);
|
||||
}
|
||||
|
||||
/// An internal trait that allows Plugin to be cloned.
|
||||
#[doc(hidden)]
|
||||
pub trait SwarmPluginClone {
|
||||
fn clone_box(&self) -> Box<dyn SwarmPlugin>;
|
||||
}
|
||||
impl<T> SwarmPluginClone for T
|
||||
where
|
||||
T: 'static + SwarmPlugin + Clone,
|
||||
{
|
||||
fn clone_box(&self) -> Box<dyn SwarmPlugin> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
impl Clone for Box<dyn SwarmPlugin> {
|
||||
fn clone(&self) -> Self {
|
||||
self.clone_box()
|
||||
}
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
use azalea::pathfinder::BlockPosGoal;
|
||||
use azalea::{prelude::*, BlockPos};
|
||||
use azalea::{prelude::*, BlockPos, Swarm, SwarmEvent};
|
||||
use azalea::{Account, Client, Event};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct State {}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct SwarmState {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
env_logger::init();
|
||||
|
@ -32,44 +35,55 @@ async fn main() -> anyhow::Result<()> {
|
|||
}
|
||||
}
|
||||
});
|
||||
} // only for #[cfg]
|
||||
}
|
||||
|
||||
// let account = Account::microsoft("example@example.com").await?;
|
||||
let account = Account::offline("bot");
|
||||
let mut accounts = Vec::new();
|
||||
let mut states = Vec::new();
|
||||
|
||||
for i in 0..5 {
|
||||
accounts.push(Account::offline(&format!("bot{}", i)));
|
||||
states.push(State::default());
|
||||
}
|
||||
|
||||
loop {
|
||||
let e = azalea::start(azalea::Options {
|
||||
account: account.clone(),
|
||||
let e = azalea::start_swarm(azalea::SwarmOptions {
|
||||
accounts: accounts.clone(),
|
||||
address: "localhost",
|
||||
state: State::default(),
|
||||
|
||||
states: states.clone(),
|
||||
swarm_state: SwarmState::default(),
|
||||
|
||||
plugins: plugins![],
|
||||
swarm_plugins: swarm_plugins![],
|
||||
|
||||
handle,
|
||||
swarm_handle,
|
||||
})
|
||||
.await;
|
||||
println!("{:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<()> {
|
||||
async fn handle(bot: Client, event: Event, _state: State) -> anyhow::Result<()> {
|
||||
match event {
|
||||
Event::Login => {
|
||||
// bot.chat("Hello world").await?;
|
||||
bot.chat("Hello world").await?;
|
||||
}
|
||||
Event::Chat(m) => {
|
||||
println!("{}", m.message().to_ansi(None));
|
||||
if m.message().to_string() == "<py5> goto" {
|
||||
let target_pos_vec3 = bot
|
||||
.dimension
|
||||
.read()
|
||||
.entity_by_uuid(&uuid::uuid!("6536bfed869548fd83a1ecd24cf2a0fd"))
|
||||
.unwrap()
|
||||
.pos()
|
||||
.clone();
|
||||
let target_pos: BlockPos = (&target_pos_vec3).into();
|
||||
// bot.look_at(&target_pos_vec3);
|
||||
bot.goto(BlockPosGoal::from(target_pos));
|
||||
// bot.walk(WalkDirection::Forward);
|
||||
}
|
||||
// println!("{}", m.message().to_ansi(None));
|
||||
// if m.message().to_string() == "<py5> goto" {
|
||||
// let target_pos_vec3 = bot
|
||||
// .dimension
|
||||
// .read()
|
||||
// .entity_by_uuid(&uuid::uuid!("6536bfed869548fd83a1ecd24cf2a0fd"))
|
||||
// .unwrap()
|
||||
// .pos()
|
||||
// .clone();
|
||||
// let target_pos: BlockPos = (&target_pos_vec3).into();
|
||||
// // bot.look_at(&target_pos_vec3);
|
||||
// bot.goto(BlockPosGoal::from(target_pos));
|
||||
// // bot.walk(WalkDirection::Forward);
|
||||
// }
|
||||
}
|
||||
Event::Initialize => {
|
||||
println!("initialized");
|
||||
|
@ -82,3 +96,11 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn swarm_handle(
|
||||
mut _swarm: Swarm,
|
||||
_event: SwarmEvent,
|
||||
_state: SwarmState,
|
||||
) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue