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:
parent
df8af1c2e1
commit
1a9d3c00f5
25 changed files with 408 additions and 140 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -114,6 +114,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"async-trait",
|
||||
"azalea-block",
|
||||
"azalea-chat",
|
||||
"azalea-client",
|
||||
"azalea-core",
|
||||
"azalea-physics",
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>),
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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
151
azalea/src/swarm/chat.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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>>;
|
||||
|
|
|
@ -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(())
|
||||
|
|
Loading…
Add table
Reference in a new issue