1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 06:16:04 +00:00

almost add SwarmEvent::Chat and new plugin system

it panics rn
This commit is contained in:
mat 2022-11-21 18:52:51 -06:00
parent df8af1c2e1
commit 1a9d3c00f5
25 changed files with 408 additions and 140 deletions

1
Cargo.lock generated
View file

@ -114,6 +114,7 @@ dependencies = [
"anyhow",
"async-trait",
"azalea-block",
"azalea-chat",
"azalea-client",
"azalea-core",
"azalea-physics",

View file

@ -1,6 +1,6 @@
use crate::{style::Style, Component};
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub struct BaseComponent {
// implements mutablecomponent
pub siblings: Vec<Component>,

View file

@ -13,7 +13,7 @@ use std::{
};
/// A chat component, basically anything you can see in chat.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub enum Component {
Text(TextComponent),
Translatable(TranslatableComponent),

View file

@ -274,7 +274,7 @@ impl TryFrom<ChatFormatting> for TextColor {
}
}
#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Style {
// these are options instead of just bools because None is different than false in this case
pub color: Option<TextColor>,

View file

@ -3,7 +3,7 @@ use std::fmt::Display;
use crate::{base_component::BaseComponent, style::ChatFormatting, Component};
/// A component that contains text that's the same in all locales.
#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct TextComponent {
pub base: BaseComponent,
pub text: String,

View file

@ -4,14 +4,14 @@ use crate::{
base_component::BaseComponent, style::Style, text_component::TextComponent, Component,
};
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub enum StringOrComponent {
String(String),
Component(Component),
}
/// A message whose content depends on the client's language.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub struct TranslatableComponent {
pub base: BaseComponent,
pub key: String,

View file

@ -12,7 +12,7 @@ use azalea_protocol::packets::game::{
use std::time::{SystemTime, UNIX_EPOCH};
/// A chat packet, either a system message or a chat message.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub enum ChatPacket {
System(ClientboundSystemChatPacket),
Player(Box<ClientboundPlayerChatPacket>),

View file

@ -1,5 +1,5 @@
pub use crate::chat::ChatPacket;
use crate::{movement::WalkDirection, plugins::Plugins, Account, PlayerInfo};
use crate::{movement::WalkDirection, plugins::PluginStates, Account, PlayerInfo};
use azalea_auth::game_profile::GameProfile;
use azalea_chat::Component;
use azalea_core::{ChunkPos, GameType, ResourceLocation, Vec3};
@ -114,7 +114,7 @@ pub struct Client {
/// Plugins are a way for other crates to add custom functionality to the
/// client and keep state. If you're not making a plugin and you're using
/// the `azalea` crate. you can ignore this field.
pub plugins: Arc<Plugins>,
pub plugins: Arc<PluginStates>,
/// A map of player uuids to their information in the tab list
pub players: Arc<RwLock<HashMap<Uuid, PlayerInfo>>>,
tasks: Arc<Mutex<Vec<JoinHandle<()>>>>,
@ -192,7 +192,7 @@ impl Client {
client_information: Arc::new(RwLock::new(ClientInformation::default())),
// The plugins can be modified by the user by replacing the plugins
// field right after this. No Mutex so the user doesn't need to .lock().
plugins: Arc::new(Plugins::new()),
plugins: Arc::new(PluginStates::default()),
players: Arc::new(RwLock::new(HashMap::new())),
tasks: Arc::new(Mutex::new(Vec::new())),
}
@ -541,7 +541,7 @@ impl Client {
debug!("Got update tags packet");
}
ClientboundGamePacket::Disconnect(p) => {
debug!("Got disconnect packet {:?}", p);
println!("Got disconnect packet {:?}", p);
}
ClientboundGamePacket::UpdateRecipes(_p) => {
debug!("Got update recipes packet");

View file

@ -21,7 +21,7 @@ pub use account::Account;
pub use client::{ChatPacket, Client, ClientInformation, Event, JoinError, PhysicsState};
pub use movement::{SprintDirection, WalkDirection};
pub use player::PlayerInfo;
pub use plugins::{Plugin, Plugins};
pub use plugins::{Plugin, PluginState, PluginStates, Plugins};
#[cfg(test)]
mod tests {

View file

@ -10,8 +10,13 @@ use std::{
type U64Hasher = BuildHasherDefault<NoHashHasher<u64>>;
// 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.
#[derive(Clone, Default)]
pub struct PluginStates {
map: Option<HashMap<TypeId, Box<dyn PluginState>, U64Hasher>>,
}
/// A map of plugin ids to PluginBuilder objects. This can then be built into a
/// [`Plugins`] object as much as you want.
///
/// If you're using azalea, you should generate this from the `plugins!` macro.
#[derive(Clone, Default)]
@ -19,6 +24,15 @@ pub struct Plugins {
map: Option<HashMap<TypeId, Box<dyn Plugin>, U64Hasher>>,
}
impl PluginStates {
pub fn get<T: PluginState>(&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 Plugins {
pub fn new() -> Self {
Self::default()
@ -34,16 +48,17 @@ impl Plugins {
.insert(TypeId::of::<T>(), Box::new(plugin));
}
pub fn get<T: Plugin>(&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>())
pub fn build(self) -> PluginStates {
let mut map = HashMap::with_hasher(BuildHasherDefault::default());
for (id, plugin) in self.map.unwrap().into_iter() {
map.insert(id, plugin.build());
}
PluginStates { map: Some(map) }
}
}
impl IntoIterator for Plugins {
type Item = Box<dyn Plugin>;
impl IntoIterator for PluginStates {
type Item = Box<dyn PluginState>;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
@ -56,16 +71,39 @@ impl IntoIterator for Plugins {
/// Plugins can keep their own personal state, listen to events, and add new functions to Client.
#[async_trait]
pub trait Plugin: Send + Sync + PluginClone + Any + 'static {
pub trait PluginState: Send + Sync + PluginClone + Any + 'static {
async fn handle(self: Box<Self>, event: Event, bot: Client);
}
/// Plugins can keep their own personal state, listen to events, and add new functions to Client.
pub trait Plugin: Send + Sync + PluginBuilderClone + Any + 'static {
fn build(&self) -> Box<dyn PluginState>;
}
/// An internal trait that allows Plugin to be cloned.
#[doc(hidden)]
pub trait PluginClone {
fn clone_box(&self) -> Box<dyn Plugin>;
fn clone_box(&self) -> Box<dyn PluginState>;
}
impl<T> PluginClone for T
where
T: 'static + PluginState + Clone,
{
fn clone_box(&self) -> Box<dyn PluginState> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn PluginState> {
fn clone(&self) -> Self {
self.clone_box()
}
}
#[doc(hidden)]
pub trait PluginBuilderClone {
fn clone_box(&self) -> Box<dyn Plugin>;
}
impl<T> PluginBuilderClone for T
where
T: 'static + Plugin + Clone,
{

View file

@ -7,12 +7,12 @@ pub struct SaltSignaturePair {
pub signature: Vec<u8>,
}
#[derive(Clone, Debug, Default, McBuf)]
#[derive(Clone, Debug, Default, McBuf, PartialEq)]
pub struct MessageSignature {
pub bytes: Vec<u8>,
}
#[derive(Clone, Debug, McBuf)]
#[derive(Clone, Debug, McBuf, PartialEq)]
pub struct SignedMessageHeader {
pub previous_signature: Option<MessageSignature>,
pub sender: Uuid,

View file

@ -8,7 +8,7 @@ use azalea_crypto::{MessageSignature, SignedMessageHeader};
use azalea_protocol_macros::ClientboundGamePacket;
use uuid::Uuid;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
#[derive(Clone, Debug, McBuf, ClientboundGamePacket, PartialEq)]
pub struct ClientboundPlayerChatPacket {
pub message: PlayerChatMessage,
pub chat_type: ChatTypeBound,
@ -25,14 +25,14 @@ pub enum ChatType {
EmoteCommand = 6,
}
#[derive(Clone, Debug, McBuf)]
#[derive(Clone, Debug, McBuf, PartialEq)]
pub struct ChatTypeBound {
pub chat_type: ChatType,
pub name: Component,
pub target_name: Option<Component>,
}
#[derive(Clone, Debug, McBuf)]
#[derive(Clone, Debug, McBuf, PartialEq)]
pub struct PlayerChatMessage {
pub signed_header: SignedMessageHeader,
pub header_signature: MessageSignature,
@ -41,7 +41,7 @@ pub struct PlayerChatMessage {
pub filter_mask: FilterMask,
}
#[derive(Clone, Debug, McBuf)]
#[derive(Clone, Debug, PartialEq, McBuf)]
pub struct SignedMessageBody {
pub content: ChatMessageContent,
pub timestamp: u64,
@ -117,7 +117,7 @@ impl ChatType {
}
}
#[derive(Clone, Debug, McBuf)]
#[derive(Clone, Debug, McBuf, PartialEq)]
pub struct LastSeenMessagesEntry {
pub profile_id: Uuid,
pub last_signature: MessageSignature,
@ -129,14 +129,14 @@ pub struct LastSeenMessagesUpdate {
pub last_received: Option<LastSeenMessagesEntry>,
}
#[derive(Clone, Debug, McBuf)]
#[derive(Clone, Debug, McBuf, PartialEq)]
pub struct ChatMessageContent {
pub plain: String,
/// Only sent if the decorated message is different than the plain.
pub decorated: Option<Component>,
}
#[derive(Clone, Debug, McBuf)]
#[derive(Clone, Debug, McBuf, PartialEq)]
pub enum FilterMask {
PassThrough,
FullyFiltered,

View file

@ -2,7 +2,7 @@ use azalea_buf::McBuf;
use azalea_chat::Component;
use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
#[derive(Clone, Debug, McBuf, ClientboundGamePacket, PartialEq)]
pub struct ClientboundSystemChatPacket {
pub content: Component,
pub overlay: bool,

View file

@ -168,9 +168,13 @@ impl LimitedChunkStorage {
///
/// # Panics
/// If the chunk is not in the render distance.
pub fn limited_get_mut(&mut self, pos: &ChunkPos) -> &mut Option<Arc<Mutex<Chunk>>> {
pub fn limited_get_mut(&mut self, pos: &ChunkPos) -> Option<&mut Option<Arc<Mutex<Chunk>>>> {
let index = self.get_index(pos);
&mut self.chunks[index]
if index >= self.chunks.len() {
None
} else {
Some(&mut self.chunks[index])
}
}
/// Get a chunk,
@ -197,7 +201,9 @@ impl LimitedChunkStorage {
// don't remove it from the shared storage, since it'll be removed
// automatically if this was the last reference
}
*self.limited_get_mut(pos) = chunk;
if let Some(chunk_mut) = self.limited_get_mut(pos) {
*chunk_mut = chunk;
}
}
}
impl WeakChunkStorage {

View file

@ -12,6 +12,7 @@ version = "0.4.0"
anyhow = "^1.0.65"
async-trait = "0.1.58"
azalea-block = {version = "0.4.0", path = "../azalea-block"}
azalea-chat = { version = "0.4.0", path = "../azalea-chat" }
azalea-client = {version = "0.4.0", path = "../azalea-client"}
azalea-core = {version = "0.4.0", path = "../azalea-core"}
azalea-physics = {version = "0.4.0", path = "../azalea-physics"}

View file

@ -20,11 +20,13 @@ async fn main() {
swarm_state: State::default(),
states,
swarm_plugins: plugins![azalea_pathfinder::Plugin::default()],
swarm_plugins: plugins![],
plugins: plugins![],
handle,
swarm_handle,
join_delay: None,
})
.await
.unwrap();

View file

@ -14,7 +14,7 @@ pub struct Plugin {
pub struct State {}
#[async_trait]
impl azalea::Plugin for Plugin {
impl azalea::PluginState for Plugin {
async fn handle(self: Box<Self>, event: Event, bot: Client) {
match event {
Event::UpdateHunger => {

View file

@ -4,9 +4,12 @@ use azalea_core::Vec3;
use parking_lot::Mutex;
use std::{f64::consts::PI, sync::Arc};
#[derive(Default, Clone)]
pub struct Plugin {
pub state: State,
#[derive(Clone, Default)]
pub struct Plugin;
impl crate::Plugin for Plugin {
fn build(&self) -> Box<dyn crate::PluginState> {
Box::new(State::default())
}
}
#[derive(Default, Clone)]
@ -14,6 +17,18 @@ pub struct State {
jumping_once: Arc<Mutex<bool>>,
}
#[async_trait]
impl crate::PluginState for State {
async fn handle(self: Box<Self>, event: Event, mut bot: Client) {
if let Event::Tick = event {
if *self.jumping_once.lock() && bot.jumping() {
*self.jumping_once.lock() = false;
bot.set_jumping(false);
}
}
}
}
pub trait BotTrait {
fn jump(&mut self);
fn look_at(&mut self, pos: &Vec3);
@ -23,7 +38,7 @@ impl BotTrait for azalea_client::Client {
/// Queue a jump for the next tick.
fn jump(&mut self) {
self.set_jumping(true);
let state = self.plugins.get::<Plugin>().unwrap().state.clone();
let state = self.plugins.get::<State>().unwrap().clone();
*state.jumping_once.lock() = true;
}
@ -34,18 +49,6 @@ impl BotTrait for azalea_client::Client {
}
}
#[async_trait]
impl crate::Plugin for Plugin {
async fn handle(self: Box<Self>, event: Event, mut bot: Client) {
if let Event::Tick = event {
if *self.state.jumping_once.lock() && bot.jumping() {
*self.state.jumping_once.lock() = false;
bot.set_jumping(false);
}
}
}
}
fn direction_looking_at(current: &Vec3, target: &Vec3) -> (f32, f32) {
// borrowed from mineflayer's Bot.lookAt because i didn't want to do math
let delta = target - current;

View file

@ -13,9 +13,12 @@ use parking_lot::Mutex;
use std::collections::VecDeque;
use std::sync::Arc;
#[derive(Default, Clone)]
pub struct Plugin {
pub state: State,
#[derive(Clone, Default)]
pub struct Plugin;
impl crate::Plugin for Plugin {
fn build(&self) -> Box<dyn crate::PluginState> {
Box::new(State::default())
}
}
#[derive(Default, Clone)]
@ -25,10 +28,10 @@ pub struct State {
}
#[async_trait]
impl crate::Plugin for Plugin {
impl crate::PluginState for State {
async fn handle(self: Box<Self>, event: Event, mut bot: Client) {
if let Event::Tick = event {
let mut path = self.state.path.lock();
let mut path = self.path.lock();
if !path.is_empty() {
tick_execute_path(&mut bot, &mut path);
@ -102,9 +105,8 @@ impl Trait for azalea_client::Client {
let state = self
.plugins
.get::<Plugin>()
.get::<State>()
.expect("Pathfinder plugin not installed!")
.state
.clone();
// convert the Option<Vec<Node>> to a VecDeque<Node>
*state.path.lock() = p.expect("no path").into_iter().collect();

View file

@ -2,5 +2,5 @@
pub use crate::bot::BotTrait;
pub use crate::pathfinder::Trait;
pub use crate::{plugins, swarm_plugins};
pub use crate::{plugins, swarm_plugins, Plugin};
pub use azalea_client::{Account, Client, Event};

View file

@ -1,5 +1,5 @@
use crate::{bot, pathfinder, HandleFn};
use azalea_client::{Account, Client, Plugin, Plugins};
use azalea_client::{Account, Client, Plugin, PluginState, PluginStates, Plugins};
use azalea_protocol::ServerAddress;
use std::{future::Future, sync::Arc};
use thiserror::Error;
@ -113,9 +113,11 @@ pub async fn start<
let (mut bot, mut rx) = Client::join(&options.account, address).await?;
let mut plugins = options.plugins;
plugins.add(bot::Plugin::default());
plugins.add(pathfinder::Plugin::default());
bot.plugins = Arc::new(plugins);
// DEFAULT PLUGINS
plugins.add(bot::Plugin);
plugins.add(pathfinder::Plugin);
bot.plugins = Arc::new(plugins.build());
let state = options.state;
@ -125,17 +127,6 @@ pub async fn start<
tokio::spawn(plugin.handle(event.clone(), bot.clone()));
}
tokio::spawn(bot::Plugin::handle(
Box::new(bot.plugins.get::<bot::Plugin>().unwrap().clone()),
event.clone(),
bot.clone(),
));
tokio::spawn(pathfinder::Plugin::handle(
Box::new(bot.plugins.get::<pathfinder::Plugin>().unwrap().clone()),
event.clone(),
bot.clone(),
));
tokio::spawn((options.handle)(bot.clone(), event.clone(), state.clone()));
}

151
azalea/src/swarm/chat.rs Normal file
View file

@ -0,0 +1,151 @@
//! Implements SwarmEvent::Chat
// How the chat event works (to avoid firing the event multiple times):
// ---
// There's a shared queue of all the chat messages
// Each bot contains an index of the farthest message we've seen
// When a bot receives a chat messages, it looks into the queue to find the
// earliest instance of the message content that's after the bot's chat index.
// If it finds it, then its personal index is simply updated. Otherwise, fire
// the event and add to the queue.
//
// To make sure the queue doesn't grow too large, we keep a `chat_min_index`
// in Swarm that's set to the smallest index of all the bots, and we remove all
// messages from the queue that are before that index.
use crate::{Swarm, SwarmEvent};
use async_trait::async_trait;
use azalea_client::{ChatPacket, Client, Event};
use parking_lot::Mutex;
use std::{collections::VecDeque, sync::Arc};
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
#[derive(Clone)]
pub struct Plugin {
pub swarm_state: SwarmState,
pub tx: UnboundedSender<ChatPacket>,
}
impl crate::Plugin for Plugin {
fn build(&self) -> Box<dyn crate::PluginState> {
Box::new(State {
farthest_chat_index: Arc::new(Mutex::new(0)),
swarm_state: self.swarm_state.clone(),
tx: self.tx.clone(),
})
}
}
#[derive(Clone)]
pub struct State {
pub farthest_chat_index: Arc<Mutex<usize>>,
pub tx: UnboundedSender<ChatPacket>,
pub swarm_state: SwarmState,
}
#[derive(Clone)]
pub struct SwarmState {
pub chat_queue: Arc<Mutex<VecDeque<ChatPacket>>>,
pub chat_min_index: Arc<Mutex<usize>>,
pub rx: Arc<tokio::sync::Mutex<UnboundedReceiver<ChatPacket>>>,
}
#[async_trait]
impl crate::PluginState for State {
async fn handle(self: Box<Self>, event: Event, _bot: Client) {
// we're allowed to access Plugin::swarm_state since it's shared for every bot
if let Event::Chat(m) = event {
// println!("bot got a message: {}", m.message().to_ansi(None));
// When a bot receives a chat messages, it looks into the queue to find the
// earliest instance of the message content that's after the bot's chat index.
// If it finds it, then its personal index is simply updated. Otherwise, fire
// the event and add to the queue.
let mut chat_queue = self.swarm_state.chat_queue.lock();
let chat_min_index = self.swarm_state.chat_min_index.lock();
let mut farthest_chat_index = self.farthest_chat_index.lock();
let actual_vec_index = *farthest_chat_index - *chat_min_index;
// println!("actual_vec_index: {}", actual_vec_index);
// go through the queue and find the first message that's after the bot's index
let mut found = false;
for (i, msg) in chat_queue.iter().enumerate().skip(actual_vec_index) {
if msg == &m {
// found the message, update the index
*farthest_chat_index = i + *chat_min_index;
found = true;
break;
}
}
if !found {
// didn't find the message, so fire the swarm event and add to the queue
// println!("new message, firing event");
self.tx
.send(m.clone())
.expect("failed to send chat message to swarm");
chat_queue.push_back(m);
}
}
}
}
impl SwarmState {
pub fn new<S>(swarm: Swarm<S>) -> (Self, UnboundedSender<ChatPacket>)
where
S: Send + Sync + Clone + 'static,
{
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let swarm_state = SwarmState {
chat_queue: Arc::new(Mutex::new(VecDeque::new())),
chat_min_index: Arc::new(Mutex::new(0)),
rx: Arc::new(tokio::sync::Mutex::new(rx)),
};
tokio::spawn(swarm_state.clone().start(swarm.clone()));
(swarm_state.clone(), tx)
}
async fn start<S>(self, swarm: Swarm<S>)
where
S: Send + Sync + Clone + 'static,
{
// it should never be locked unless we reused the same plugin for two swarms (bad)
let mut rx = self.rx.lock().await;
while let Some(m) = rx.recv().await {
// println!("received event, firing to swarm");
swarm.swarm_tx.send(SwarmEvent::Chat(m)).unwrap();
// To make sure the queue doesn't grow too large, we keep a `chat_min_index`
// in Swarm that's set to the smallest index of all the bots, and we remove all
// messages from the queue that are before that index.
let mut chat_queue = self.chat_queue.lock();
let mut chat_min_index = self.chat_min_index.lock();
let mut new_chat_min_index = usize::MAX;
for (bot, _) in swarm.bot_datas.lock().iter() {
let this_farthest_chat_index = *bot
.plugins
.get::<State>()
.expect("Chat plugin not installed")
.farthest_chat_index
.lock();
if this_farthest_chat_index < new_chat_min_index {
new_chat_min_index = this_farthest_chat_index;
}
}
// remove all messages from the queue that are before the min index
while let Some((i, _m)) = chat_queue.iter().enumerate().next() {
if i + *chat_min_index < new_chat_min_index {
chat_queue.pop_front();
} else {
break;
}
}
// update the min index
*chat_min_index = new_chat_min_index;
}
}
}

View file

@ -1,8 +1,10 @@
mod chat;
mod plugins;
pub use self::plugins::*;
use crate::{bot, HandleFn};
use azalea_client::{Account, Client, Event, JoinError, Plugins};
use azalea_chat::Component;
use azalea_client::{Account, ChatPacket, Client, Event, JoinError, Plugin, PluginStates, Plugins};
use azalea_protocol::{
connect::{Connection, ConnectionError},
resolver::{self, ResolverError},
@ -12,10 +14,9 @@ use azalea_world::WeakWorldContainer;
use futures::future::join_all;
use log::error;
use parking_lot::{Mutex, RwLock};
use std::{future::Future, net::SocketAddr, sync::Arc, time::Duration};
use std::{collections::VecDeque, future::Future, net::SocketAddr, sync::Arc, time::Duration};
use thiserror::Error;
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
use uuid::Uuid;
/// A helper macro that generates a [`Plugins`] struct from a list of objects
/// that implement [`Plugin`].
@ -64,11 +65,16 @@ pub struct Swarm<S> {
pub enum SwarmEvent {
/// All the bots in the swarm have successfully joined the server.
Login,
/// The swarm was created. This is only fired once, and it's guaranteed to
/// be the first event to fire.
Init,
/// A bot got disconnected from the server.
///
/// You can implement an auto-reconnect by calling [`Swarm::add`]
/// with the account from this event.
Disconnect(Account),
/// At least one bot received a chat message.
Chat(ChatPacket),
}
pub type SwarmHandleFn<Fut, S, SS> = fn(Swarm<S>, SwarmEvent, SS) -> Fut;
@ -145,9 +151,15 @@ pub async fn start_swarm<
let world_container = Arc::new(RwLock::new(WeakWorldContainer::default()));
let mut plugins = options.plugins;
plugins.add(bot::Plugin::default());
let swarm_plugins = options.swarm_plugins;
let (bots_tx, mut bots_rx) = mpsc::unbounded_channel();
// DEFAULT CLIENT PLUGINS
plugins.add(bot::Plugin);
plugins.add(crate::pathfinder::Plugin);
// DEFAULT SWARM PLUGINS
// we can't modify the swarm plugins after this
let (bots_tx, bots_rx) = mpsc::unbounded_channel();
let (swarm_tx, mut swarm_rx) = mpsc::unbounded_channel();
let mut swarm = Swarm {
@ -164,13 +176,22 @@ pub async fn start_swarm<
swarm_tx: swarm_tx.clone(),
};
{
// the chat plugin is hacky and needs the swarm to be passed like this
let (chat_swarm_state, chat_tx) = chat::SwarmState::new(swarm.clone());
swarm.plugins.add(chat::Plugin {
swarm_state: chat_swarm_state,
tx: chat_tx,
});
}
let swarm_plugins = swarm_plugins.build();
let mut swarm_clone = swarm.clone();
let join_task = tokio::spawn(async move {
if let Some(join_delay) = options.join_delay {
// if there's a join delay, then join one by one
for (account, state) in options.accounts.iter().zip(options.states) {
// exponential backoff
let mut disconnects = 0;
swarm_clone
.add_with_exponential_backoff(account, state.clone())
.await;
@ -180,8 +201,6 @@ pub async fn start_swarm<
let swarm_borrow = &swarm_clone;
join_all(options.accounts.iter().zip(options.states).map(
async move |(account, state)| -> Result<(), JoinError> {
// exponential backoff
let mut disconnects = 0;
swarm_borrow
.clone()
.add_with_exponential_backoff(account, state.clone())
@ -198,10 +217,10 @@ pub async fn start_swarm<
// Watch swarm_rx and send those events to the plugins and swarm_handle.
let swarm_clone = swarm.clone();
let swarm_plugins_clone = swarm_plugins.clone();
tokio::spawn(async move {
while let Some(event) = swarm_rx.recv().await {
let cloned_swarm_plugins = options.swarm_plugins.clone();
for plugin in cloned_swarm_plugins.into_iter() {
for plugin in swarm_plugins_clone.clone().into_iter() {
tokio::spawn(plugin.handle(event.clone(), swarm_clone.clone()));
}
tokio::spawn((options.swarm_handle)(
@ -262,7 +281,7 @@ where
tx.send(Event::Initialize).unwrap();
bot.start_tasks(tx);
bot.plugins = Arc::new(self.plugins.clone());
bot.plugins = Arc::new(self.plugins.clone().build());
let cloned_bots_tx = self.bots_tx.clone();
let cloned_bot = bot.clone();
@ -270,6 +289,8 @@ where
let owned_account = account.clone();
let bot_datas = self.bot_datas.clone();
let swarm_tx = self.swarm_tx.clone();
// send the init event immediately so it's the first thing we get
swarm_tx.send(SwarmEvent::Init).unwrap();
tokio::spawn(async move {
while let Some(event) = rx.recv().await {
// we can't handle events here (since we can't copy the handler),

View file

@ -17,7 +17,26 @@ pub struct SwarmPlugins<S> {
map: Option<HashMap<TypeId, Box<dyn SwarmPlugin<S>>, BuildHasherDefault<NoHashHasher<u64>>>>,
}
impl<S> SwarmPlugins<S> {
#[derive(Clone)]
pub struct SwarmPluginStates<S> {
map: Option<
HashMap<TypeId, Box<dyn SwarmPluginState<S>>, BuildHasherDefault<NoHashHasher<u64>>>,
>,
}
impl<S> SwarmPluginStates<S> {
pub fn get<T: SwarmPluginState<S>>(&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<S> SwarmPlugins<S>
where
S: 'static,
{
pub fn new() -> Self {
Self { map: None }
}
@ -32,16 +51,20 @@ impl<S> SwarmPlugins<S> {
.insert(TypeId::of::<T>(), Box::new(plugin));
}
pub fn get<T: SwarmPlugin<S>>(&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>())
pub fn build(self) -> SwarmPluginStates<S> {
if self.map.is_none() {
return SwarmPluginStates { map: None };
}
let mut map = HashMap::with_hasher(BuildHasherDefault::default());
for (id, plugin) in self.map.unwrap().into_iter() {
map.insert(id, plugin.build());
}
SwarmPluginStates { map: Some(map) }
}
}
impl<S> IntoIterator for SwarmPlugins<S> {
type Item = Box<dyn SwarmPlugin<S>>;
impl<S> IntoIterator for SwarmPluginStates<S> {
type Item = Box<dyn SwarmPluginState<S>>;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
@ -54,11 +77,35 @@ impl<S> IntoIterator for SwarmPlugins<S> {
/// Plugins can keep their own personal state, listen to events, and add new functions to Client.
#[async_trait]
pub trait SwarmPlugin<S>: Send + Sync + SwarmPluginClone<S> + Any + 'static {
pub trait SwarmPluginState<S>: Send + Sync + SwarmPluginStateClone<S> + Any + 'static {
async fn handle(self: Box<Self>, event: SwarmEvent, swarm: Swarm<S>);
}
/// An internal trait that allows Plugin to be cloned.
#[async_trait]
pub trait SwarmPlugin<S>: Send + Sync + SwarmPluginClone<S> + Any + 'static {
fn build(&self) -> Box<dyn SwarmPluginState<S>>;
}
/// An internal trait that allows SwarmPluginState to be cloned.
#[doc(hidden)]
pub trait SwarmPluginStateClone<S> {
fn clone_box(&self) -> Box<dyn SwarmPluginState<S>>;
}
impl<T, S> SwarmPluginStateClone<S> for T
where
T: 'static + SwarmPluginState<S> + Clone,
{
fn clone_box(&self) -> Box<dyn SwarmPluginState<S>> {
Box::new(self.clone())
}
}
impl<S> Clone for Box<dyn SwarmPluginState<S>> {
fn clone(&self) -> Self {
self.clone_box()
}
}
/// An internal trait that allows SwarmPlugin to be cloned.
#[doc(hidden)]
pub trait SwarmPluginClone<S> {
fn clone_box(&self) -> Box<dyn SwarmPlugin<S>>;

View file

@ -46,7 +46,7 @@ async fn main() -> anyhow::Result<()> {
let mut accounts = Vec::new();
let mut states = Vec::new();
for i in 0..10 {
for i in 0..5 {
accounts.push(Account::offline(&format!("bot{}", i)));
states.push(State::default());
}
@ -65,7 +65,7 @@ async fn main() -> anyhow::Result<()> {
handle,
swarm_handle,
join_delay: None,
join_delay: Some(Duration::from_millis(100)),
})
.await;
println!("{e:?}");
@ -78,54 +78,56 @@ async fn handle(mut bot: Client, event: Event, state: State) -> anyhow::Result<(
bot.chat("Hello world").await?;
}
Event::Chat(m) => {
if m.content() == bot.profile.name {
bot.chat("Bye").await?;
tokio::time::sleep(Duration::from_millis(50)).await;
bot.disconnect().await?;
}
// println!("{}", m.message().to_ansi(None));
// if m.message().to_string() == "<py5> goto" {
// let target_pos_vec3 = bot
// .world
// .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);
// if m.content() == bot.profile.name {
// bot.chat("Bye").await?;
// tokio::time::sleep(Duration::from_millis(50)).await;
// bot.disconnect().await?;
// }
// println!("{}", m.message().to_ansi(None));
if m.message().to_string() == "<py5> goto" {
let entity = bot
.world
.read()
.entity_by_uuid(&uuid::uuid!("6536bfed-8695-48fd-83a1-ecd24cf2a0fd"));
println!("entity: {:?}", entity);
if let Some(entity) = entity {
let target_pos_vec3 = entity.pos().clone();
let target_pos: BlockPos = (&target_pos_vec3).into();
println!("target_pos: {:?}", target_pos);
// bot.look_at(&target_pos_vec3);
bot.goto(BlockPosGoal::from(target_pos));
// bot.walk(WalkDirection::Forward);
}
}
}
Event::Initialize => {
println!("initialized");
}
Event::Tick => {
// look in a random direction and walk for 1-3 seconds
{
let mut moving = state.moving.lock();
if *moving {
return Ok(());
}
*moving = true;
}
// {
// let mut moving = state.moving.lock();
// if *moving {
// return Ok(());
// }
// *moving = true;
// }
let rotation = rand::thread_rng().gen_range(0.0..360.0);
let duration = rand::thread_rng().gen_range(1..3);
let jumping = rand::thread_rng().gen_bool(0.5);
// let rotation = rand::thread_rng().gen_range(0.0..360.0);
// let duration = rand::thread_rng().gen_range(1..3);
// let jumping = rand::thread_rng().gen_bool(0.5);
bot.set_rotation(rotation, 0.);
bot.walk(WalkDirection::Forward);
if jumping {
bot.set_jumping(true);
}
tokio::time::sleep(tokio::time::Duration::from_secs(duration)).await;
bot.walk(WalkDirection::None);
if jumping {
bot.set_jumping(false);
}
*state.moving.lock() = false;
// bot.set_rotation(rotation, 0.);
// bot.walk(WalkDirection::Forward);
// if jumping {
// bot.set_jumping(true);
// }
// tokio::time::sleep(tokio::time::Duration::from_secs(duration)).await;
// bot.walk(WalkDirection::None);
// if jumping {
// bot.set_jumping(false);
// }
// *state.moving.lock() = false;
}
_ => {}
}
@ -144,6 +146,9 @@ async fn swarm_handle(
tokio::time::sleep(Duration::from_secs(5)).await;
swarm.add(account, State::default()).await?;
}
SwarmEvent::Chat(m) => {
// println!("swarm chat message: {}", m.message().to_ansi(None));
}
_ => {}
}
Ok(())