From 631ed63dbdc7167df4de02a55b5c2ef1cea909e9 Mon Sep 17 00:00:00 2001 From: mat <27899617+mat-1@users.noreply.github.com> Date: Sun, 27 Nov 2022 16:25:07 -0600 Subject: [PATCH] Swarm (#36) * make azalea-pathfinder dir * start writing d* lite impl * more work on d* lite * work more on implementing d* lite * full d* lite impl * updated edges * add next() function * add NoPathError * why does dstar lite not work * fix d* lite implementation * make the test actually check the coords * replace while loop with if statement * fix clippy complaints * make W only have to be PartialOrd * fix PartialOrd issues * implement mtd* lite * add a test to mtd* lite * remove normal d* lite * make heuristic only take in one arg * add `success` function * Update README.md * evil black magic to make .entity not need dimension * start adding moves * slightly improve the vec3/position situation new macro that implements all the useful functions * moves stuff * make it compile * update deps in az-pathfinder * make it compile again * more pathfinding stuff * add Bot::look_at * replace EntityMut and EntityRef with just Entity * block pos pathfinding stuff * rename movedirection to walkdirection * execute path every tick * advance path * change az-pf version * make azalea_client keep plugin state * fix Plugins::get * why does it think there is air * start debugging incorrect air * update some From methods to use rem_euclid * start adding swarm * fix deadlock i still don't understand why it was happening but the solution was to keep the Client::player lock for shorter so it didn't overlap with the Client::dimension lock * make lookat actually work probably * fix going too fast * Update main.rs * make a thing immutable * direction_looking_at * fix rotations * import swarm in an example * fix stuff from merge * remove azalea_pathfinder import * delete azalea-pathfinder crate already in azalea::pathfinder module * swarms * start working on shared dimensions * Shared worlds work * start adding Swarm::add_account * add_account works * change "client" to "bot" in some places * Fix issues from merge * Update world.rs * add SwarmEvent::Disconnect(Account) * almost add SwarmEvent::Chat and new plugin system it panics rn * make plugins have to provide the State associated type * improve comments * make fn build slightly cleaner * fix SwarmEvent::Chat * change a println in bot/main.rs * Client::shutdown -> disconnect * polish fix clippy warnings + improve some docs a bit * fix shared worlds* *there's a bug that entities and bots will have their positions exaggerated because the relative movement packet is applied for every entity once per bot * i am being trolled by rust for some reason some stuff is really slow for literally no reason and it makes no sense i am going insane * make world an RwLock again * remove debug messages * fix skipping event ticks unfortunately now sending events is `.send().await?` instead of just `.send()` * fix deadlock + warnings * turns out my floor_mod impl was wrong and i32::rem_euclid has the correct behavior LOL * still errors with lots of bots * make swarm iter & fix new chunks not loading * improve docs * start fixing tests * fix all the tests except the examples i don't know how to exclude them from the tests * improve docs some more --- Cargo.lock | 14 +- azalea-auth/src/auth.rs | 3 +- azalea-chat/src/base_component.rs | 2 +- azalea-chat/src/component.rs | 5 +- azalea-chat/src/style.rs | 2 +- azalea-chat/src/text_component.rs | 2 +- azalea-chat/src/translatable_component.rs | 4 +- azalea-client/Cargo.toml | 16 +- azalea-client/src/chat.rs | 25 +- azalea-client/src/client.rs | 500 +++++++++++------- azalea-client/src/lib.rs | 6 +- azalea-client/src/movement.rs | 39 +- azalea-client/src/player.rs | 17 - azalea-client/src/plugins.rs | 130 +++-- azalea-core/src/direction.rs | 8 +- azalea-core/src/lib.rs | 10 - azalea-crypto/src/signing.rs | 4 +- azalea-physics/Cargo.toml | 0 azalea-physics/src/collision/mod.rs | 0 .../src/collision/world_collisions.rs | 4 +- azalea-physics/src/lib.rs | 10 +- azalea-protocol/src/connect.rs | 73 ++- azalea-protocol/src/lib.rs | 17 +- .../game/clientbound_player_chat_packet.rs | 14 +- .../game/clientbound_system_chat_packet.rs | 2 +- azalea-world/Cargo.toml | 0 azalea-world/src/chunk_storage.rs | 167 ++++-- azalea-world/src/container.rs | 54 ++ azalea-world/src/entity/attributes.rs | 2 +- azalea-world/src/entity/mod.rs | 13 +- azalea-world/src/entity_storage.rs | 328 +++++++++--- azalea-world/src/lib.rs | 170 +----- azalea-world/src/world.rs | 181 +++++++ azalea/Cargo.toml | 12 +- azalea/README.md | 1 + azalea/examples/mine_a_chunk.rs | 29 +- azalea/examples/potatobot/autoeat.rs | 2 +- azalea/examples/potatobot/main.rs | 2 +- azalea/examples/pvp.rs | 4 +- azalea/src/bot.rs | 37 +- azalea/src/lib.rs | 149 +----- azalea/src/pathfinder/mod.rs | 34 +- azalea/src/prelude.rs | 2 +- azalea/src/start.rs | 136 +++++ azalea/src/swarm/chat.rs | 147 +++++ azalea/src/swarm/mod.rs | 447 ++++++++++++++++ azalea/src/swarm/plugins.rs | 134 +++++ bot/Cargo.toml | 2 + bot/src/main.rs | 132 ++++- 49 files changed, 2262 insertions(+), 830 deletions(-) mode change 100755 => 100644 azalea-client/src/lib.rs mode change 100755 => 100644 azalea-client/src/movement.rs mode change 100755 => 100644 azalea-physics/Cargo.toml mode change 100755 => 100644 azalea-physics/src/collision/mod.rs mode change 100755 => 100644 azalea-physics/src/lib.rs mode change 100755 => 100644 azalea-protocol/src/lib.rs mode change 100755 => 100644 azalea-world/Cargo.toml create mode 100644 azalea-world/src/container.rs mode change 100755 => 100644 azalea-world/src/lib.rs create mode 100644 azalea-world/src/world.rs mode change 100755 => 100644 azalea/Cargo.toml mode change 100755 => 100644 azalea/examples/mine_a_chunk.rs mode change 100755 => 100644 azalea/src/bot.rs mode change 100755 => 100644 azalea/src/lib.rs mode change 100755 => 100644 azalea/src/prelude.rs create mode 100644 azalea/src/start.rs create mode 100644 azalea/src/swarm/chat.rs create mode 100644 azalea/src/swarm/mod.rs create mode 100644 azalea/src/swarm/plugins.rs mode change 100755 => 100644 bot/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 7bbb36ab..132e2eb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,18 +114,22 @@ dependencies = [ "anyhow", "async-trait", "azalea-block", + "azalea-chat", "azalea-client", "azalea-core", "azalea-physics", "azalea-protocol", "azalea-world", "env_logger", + "futures", "log", + "nohash-hasher", "num-traits", "parking_lot", "priority-queue", "thiserror", "tokio", + "uuid", ] [[package]] @@ -402,8 +406,10 @@ version = "0.2.0" dependencies = [ "anyhow", "azalea", + "azalea-protocol", "env_logger", "parking_lot", + "rand", "tokio", "uuid", ] @@ -467,9 +473,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" dependencies = [ "num-integer", "num-traits", @@ -2085,9 +2091,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" [[package]] name = "vcpkg" diff --git a/azalea-auth/src/auth.rs b/azalea-auth/src/auth.rs index b7f834d4..dbdf3f0f 100755 --- a/azalea-auth/src/auth.rs +++ b/azalea-auth/src/auth.rs @@ -209,6 +209,7 @@ pub struct GameOwnershipItem { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ProfileResponse { + // todo: make the id a uuid pub id: String, pub name: String, pub skins: Vec, @@ -463,7 +464,7 @@ pub enum GetProfileError { Http(#[from] reqwest::Error), } -async fn get_profile( +pub async fn get_profile( client: &reqwest::Client, minecraft_access_token: &str, ) -> Result { diff --git a/azalea-chat/src/base_component.rs b/azalea-chat/src/base_component.rs index c2f3513d..e532de11 100755 --- a/azalea-chat/src/base_component.rs +++ b/azalea-chat/src/base_component.rs @@ -1,6 +1,6 @@ use crate::{style::Style, Component}; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct BaseComponent { // implements mutablecomponent pub siblings: Vec, diff --git a/azalea-chat/src/component.rs b/azalea-chat/src/component.rs index 882a521a..9362a66b 100755 --- a/azalea-chat/src/component.rs +++ b/azalea-chat/src/component.rs @@ -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), @@ -63,13 +63,14 @@ impl Component { /// /// ```rust /// use azalea_chat::Component; + /// use serde::de::Deserialize; /// /// let component = Component::deserialize(&serde_json::json!({ /// "text": "Hello, world!", /// "color": "red", /// })).unwrap(); /// - /// println!("{}", component.to_ansi()); + /// println!("{}", component.to_ansi(None)); /// ``` pub fn to_ansi(&self, default_style: Option<&Style>) -> String { // default the default_style to white if it's not set diff --git a/azalea-chat/src/style.rs b/azalea-chat/src/style.rs index 1243d56f..cdf8f86f 100755 --- a/azalea-chat/src/style.rs +++ b/azalea-chat/src/style.rs @@ -274,7 +274,7 @@ impl TryFrom 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, diff --git a/azalea-chat/src/text_component.rs b/azalea-chat/src/text_component.rs index eea66bb7..0d88ca05 100755 --- a/azalea-chat/src/text_component.rs +++ b/azalea-chat/src/text_component.rs @@ -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, diff --git a/azalea-chat/src/translatable_component.rs b/azalea-chat/src/translatable_component.rs index d187adda..28725c44 100755 --- a/azalea-chat/src/translatable_component.rs +++ b/azalea-chat/src/translatable_component.rs @@ -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, diff --git a/azalea-client/Cargo.toml b/azalea-client/Cargo.toml index ec26d596..0efdeef2 100644 --- a/azalea-client/Cargo.toml +++ b/azalea-client/Cargo.toml @@ -11,14 +11,14 @@ version = "0.4.0" [dependencies] anyhow = "1.0.59" async-trait = "0.1.58" -azalea-auth = {path = "../azalea-auth", version = "0.4.0" } -azalea-block = {path = "../azalea-block", version = "0.4.0" } -azalea-chat = {path = "../azalea-chat", version = "0.4.0" } -azalea-core = {path = "../azalea-core", version = "0.4.0" } -azalea-crypto = {path = "../azalea-crypto", version = "0.4.0" } -azalea-physics = {path = "../azalea-physics", version = "0.4.0" } -azalea-protocol = {path = "../azalea-protocol", version = "0.4.0" } -azalea-world = {path = "../azalea-world", version = "0.4.0" } +azalea-auth = {path = "../azalea-auth", version = "0.4.0"} +azalea-block = {path = "../azalea-block", version = "0.4.0"} +azalea-chat = {path = "../azalea-chat", version = "0.4.0"} +azalea-core = {path = "../azalea-core", version = "0.4.0"} +azalea-crypto = {path = "../azalea-crypto", version = "0.4.0"} +azalea-physics = {path = "../azalea-physics", version = "0.4.0"} +azalea-protocol = {path = "../azalea-protocol", version = "0.4.0"} +azalea-world = {path = "../azalea-world", version = "0.4.0"} log = "0.4.17" nohash-hasher = "0.2.0" once_cell = "1.16.0" diff --git a/azalea-client/src/chat.rs b/azalea-client/src/chat.rs index 01236630..5f566fe7 100755 --- a/azalea-client/src/chat.rs +++ b/azalea-client/src/chat.rs @@ -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), @@ -126,28 +126,9 @@ impl Client { /// Send a message in chat. /// - /// # Examples - /// /// ```rust,no_run - /// # use azalea::prelude::*; - /// # use parking_lot::Mutex; - /// # use std::sync::Arc; - /// # #[tokio::main] - /// # async fn main() { - /// # let account = Account::offline("bot"); - /// # azalea::start(azalea::Options { - /// # account, - /// # address: "localhost", - /// # state: State::default(), - /// # plugins: plugins![], - /// # handle, - /// # }) - /// # .await - /// # .unwrap(); - /// # } - /// # #[derive(Default, Clone)] - /// # pub struct State {} - /// # async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> { + /// # use azalea_client::{Client, Event}; + /// # async fn handle(bot: Client, event: Event) -> anyhow::Result<()> { /// bot.chat("Hello, world!").await.unwrap(); /// # Ok(()) /// # } diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 675f8bec..ce4ca4cf 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -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}; @@ -15,7 +15,10 @@ use azalea_protocol::{ serverbound_move_player_pos_rot_packet::ServerboundMovePlayerPosRotPacket, ClientboundGamePacket, ServerboundGamePacket, }, - handshake::client_intention_packet::ClientIntentionPacket, + handshake::{ + client_intention_packet::ClientIntentionPacket, ClientboundHandshakePacket, + ServerboundHandshakePacket, + }, login::{ serverbound_custom_query_packet::ServerboundCustomQueryPacket, serverbound_hello_packet::ServerboundHelloPacket, @@ -29,9 +32,9 @@ use azalea_protocol::{ }; use azalea_world::{ entity::{metadata, Entity, EntityData, EntityMetadata}, - World, + WeakWorld, WeakWorldContainer, World, }; -use log::{debug, error, info, warn}; +use log::{debug, error, info, trace, warn}; use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; use std::{ collections::HashMap, @@ -41,7 +44,7 @@ use std::{ }; use thiserror::Error; use tokio::{ - sync::mpsc::{self, UnboundedReceiver, UnboundedSender}, + sync::mpsc::{self, Receiver, Sender}, task::JoinHandle, time::{self}, }; @@ -57,7 +60,7 @@ pub enum Event { /// it's actually spawned. This can be useful for setting the client /// information with `Client::set_client_information`, so the packet /// doesn't have to be sent twice. - Initialize, + Init, Login, Chat(ChatPacket), /// Happens 20 times per second, but only when the world is loaded. @@ -102,14 +105,20 @@ pub struct Client { pub read_conn: Arc>>, pub write_conn: Arc>>, pub entity_id: Arc>, + /// The world that this client has access to. This supports shared worlds. pub world: Arc>, + /// A container of world names to worlds. If we're not using a shared world + /// (i.e. not a swarm), then this will only contain data about the world + /// we're currently in. + world_container: Arc>, + pub world_name: Arc>>, pub physics_state: Arc>, pub client_information: Arc>, pub dead: Arc>, /// 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, + pub plugins: Arc, /// A map of player uuids to their information in the tab list pub players: Arc>>, tasks: Arc>>>, @@ -152,13 +161,50 @@ pub enum JoinError { pub enum HandleError { #[error("{0}")] Poison(String), - #[error("{0}")] + #[error(transparent)] Io(#[from] io::Error), #[error(transparent)] Other(#[from] anyhow::Error), + #[error("{0}")] + Send(#[from] mpsc::error::SendError), } impl Client { + /// Create a new client from the given GameProfile, Connection, and World. + /// You should only use this if you want to change these fields from the + /// defaults, otherwise use [`Client::join`]. + pub fn new( + profile: GameProfile, + conn: Connection, + world_container: Option>>, + ) -> Self { + let (read_conn, write_conn) = conn.into_split(); + let (read_conn, write_conn) = ( + Arc::new(tokio::sync::Mutex::new(read_conn)), + Arc::new(tokio::sync::Mutex::new(write_conn)), + ); + + Self { + profile, + read_conn, + write_conn, + // default our id to 0, it'll be set later + entity_id: Arc::new(RwLock::new(0)), + world: Arc::new(RwLock::new(World::default())), + world_container: world_container + .unwrap_or_else(|| Arc::new(RwLock::new(WeakWorldContainer::new()))), + world_name: Arc::new(RwLock::new(None)), + physics_state: Arc::new(Mutex::new(PhysicsState::default())), + client_information: Arc::new(RwLock::new(ClientInformation::default())), + dead: Arc::new(Mutex::new(false)), + // 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(PluginStates::default()), + players: Arc::new(RwLock::new(HashMap::new())), + tasks: Arc::new(Mutex::new(Vec::new())), + } + } + /// Connect to a Minecraft server. /// /// To change the render distance and other settings, use @@ -168,26 +214,56 @@ impl Client { /// # Examples /// /// ```rust,no_run - /// use azalea_client::Client; + /// use azalea_client::{Client, Account}; /// /// #[tokio::main] - /// async fn main() -> Box { + /// async fn main() -> Result<(), Box> { /// let account = Account::offline("bot"); /// let (client, rx) = Client::join(&account, "localhost").await?; /// client.chat("Hello, world!").await?; - /// client.shutdown().await?; + /// client.disconnect().await?; + /// Ok(()) /// } /// ``` pub async fn join( account: &Account, address: impl TryInto, - ) -> Result<(Self, UnboundedReceiver), JoinError> { + ) -> Result<(Self, Receiver), JoinError> { let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?; - let resolved_address = resolver::resolve_address(&address).await?; - let mut conn = Connection::new(&resolved_address).await?; + let conn = Connection::new(&resolved_address).await?; + let (conn, game_profile) = Self::handshake(conn, account, &address).await?; + // The buffer has to be 1 to avoid a bug where if it lags events are + // received a bit later instead of the instant they were fired. + // That bug especially causes issues with the pathfinder. + let (tx, rx) = mpsc::channel(1); + + // we got the GameConnection, so the server is now connected :) + let client = Client::new(game_profile, conn, None); + + tx.send(Event::Init).await.expect("Failed to send event"); + + // just start up the game loop and we're ready! + + client.start_tasks(tx); + + Ok((client, rx)) + } + + /// Do a handshake with the server and get to the game state from the initial handshake state. + pub async fn handshake( + mut conn: Connection, + account: &Account, + address: &ServerAddress, + ) -> Result< + ( + Connection, + GameProfile, + ), + JoinError, + > { // handshake conn.write( ClientIntentionPacket { @@ -267,48 +343,7 @@ impl Client { } }; - let (read_conn, write_conn) = conn.into_split(); - - let read_conn = Arc::new(tokio::sync::Mutex::new(read_conn)); - let write_conn = Arc::new(tokio::sync::Mutex::new(write_conn)); - - let (tx, rx) = mpsc::unbounded_channel(); - - // we got the GameConnection, so the server is now connected :) - let client = Client { - profile, - read_conn, - write_conn, - // default our id to 0, it'll be set later - entity_id: Arc::new(RwLock::new(0)), - world: Arc::new(RwLock::new(World::default())), - physics_state: Arc::new(Mutex::new(PhysicsState::default())), - client_information: Arc::new(RwLock::new(ClientInformation::default())), - dead: Arc::new(Mutex::new(false)), - // 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()), - players: Arc::new(RwLock::new(HashMap::new())), - tasks: Arc::new(Mutex::new(Vec::new())), - }; - - tx.send(Event::Initialize).unwrap(); - - // just start up the game loop and we're ready! - - // if you get an error right here that means you're doing something with locks wrong - // read the error to see where the issue is - // you might be able to just drop the lock or put it in its own scope to fix - { - let mut tasks = client.tasks.lock(); - tasks.push(tokio::spawn(Self::protocol_loop( - client.clone(), - tx.clone(), - ))); - tasks.push(tokio::spawn(Self::game_tick_loop(client.clone(), tx))); - } - - Ok((client, rx)) + Ok((conn, profile)) } /// Write a packet directly to the server. @@ -317,8 +352,8 @@ impl Client { Ok(()) } - /// Disconnect from the server, ending all tasks. - pub async fn shutdown(&self) -> Result<(), std::io::Error> { + /// Disconnect this client from the server, ending all tasks. + pub async fn disconnect(&self) -> Result<(), std::io::Error> { if let Err(e) = self.write_conn.lock().await.shutdown().await { warn!( "Error shutting down connection, but it might be fine: {}", @@ -332,7 +367,22 @@ impl Client { Ok(()) } - async fn protocol_loop(client: Client, tx: UnboundedSender) { + /// Start the protocol and game tick loop. + #[doc(hidden)] + pub fn start_tasks(&self, tx: Sender) { + // if you get an error right here that means you're doing something with locks wrong + // read the error to see where the issue is + // you might be able to just drop the lock or put it in its own scope to fix + + let mut tasks = self.tasks.lock(); + tasks.push(tokio::spawn(Client::protocol_loop( + self.clone(), + tx.clone(), + ))); + tasks.push(tokio::spawn(Client::game_tick_loop(self.clone(), tx))); + } + + async fn protocol_loop(client: Client, tx: Sender) { loop { let r = client.read_conn.lock().await.read().await; match r { @@ -340,9 +390,7 @@ impl Client { Ok(_) => {} Err(e) => { error!("Error handling packet: {}", e); - if IGNORE_ERRORS { - continue; - } else { + if !IGNORE_ERRORS { panic!("Error handling packet: {e}"); } } @@ -350,16 +398,15 @@ impl Client { Err(e) => { if let ReadPacketError::ConnectionClosed = e { info!("Connection closed"); - if let Err(e) = client.shutdown().await { + if let Err(e) = client.disconnect().await { error!("Error shutting down connection: {:?}", e); } - return; + break; } if IGNORE_ERRORS { warn!("{}", e); - match e { - ReadPacketError::FrameSplitter { .. } => panic!("Error: {e:?}"), - _ => continue, + if let ReadPacketError::FrameSplitter { .. } = e { + panic!("Error: {e:?}"); } } else { panic!("{}", e); @@ -372,12 +419,12 @@ impl Client { async fn handle( packet: &ClientboundGamePacket, client: &Client, - tx: &UnboundedSender, + tx: &Sender, ) -> Result<(), HandleError> { - tx.send(Event::Packet(Box::new(packet.clone()))).unwrap(); + tx.send(Event::Packet(Box::new(packet.clone()))).await?; match packet { ClientboundGamePacket::Login(p) => { - debug!("Got login packet {:?}", p); + debug!("Got login packet"); { // // write p into login.txt @@ -440,16 +487,27 @@ impl Client { .as_int() .expect("min_y tag is not an int"); + // add this world to the world_container (or don't if it's already there) + let weak_world = + client + .world_container + .write() + .insert(p.dimension.clone(), height, min_y); + // set the loaded_world to an empty world + // (when we add chunks or entities those will be in the world_container) let mut world_lock = client.world.write(); - // the 16 here is our render distance - // i'll make this an actual setting later - *world_lock = World::new(16, height, min_y); + *world_lock = World::new( + client.client_information.read().view_distance.into(), + weak_world, + p.player_id, + ); let entity = EntityData::new( client.profile.uuid, Vec3::default(), EntityMetadata::Player(metadata::Player::default()), ); + // make it so other entities don't update this entity in a shared world world_lock.add_entity(p.player_id, entity); *client.entity_id.write() = p.player_id; @@ -476,7 +534,7 @@ impl Client { ) .await?; - tx.send(Event::Login).unwrap(); + tx.send(Event::Login).await?; } ClientboundGamePacket::SetChunkCacheRadius(p) => { debug!("Got set chunk cache radius packet {:?}", p); @@ -501,7 +559,7 @@ impl Client { } ClientboundGamePacket::Disconnect(p) => { debug!("Got disconnect packet {:?}", p); - client.shutdown().await?; + client.disconnect().await?; } ClientboundGamePacket::UpdateRecipes(_p) => { debug!("Got update recipes packet"); @@ -521,9 +579,7 @@ impl Client { let mut world_lock = client.world.write(); - let mut player_entity = world_lock - .entity_mut(player_entity_id) - .expect("Player entity doesn't exist"); + let mut player_entity = world_lock.entity_mut(player_entity_id).unwrap(); let delta_movement = player_entity.delta; @@ -604,94 +660,102 @@ impl Client { use azalea_protocol::packets::game::clientbound_player_info_packet::Action; debug!("Got player info packet {:?}", p); - let mut players_lock = client.players.write(); - match &p.action { - Action::AddPlayer(players) => { - for player in players { - let player_info = PlayerInfo { - profile: GameProfile { + let mut events = Vec::new(); + { + let mut players_lock = client.players.write(); + match &p.action { + Action::AddPlayer(players) => { + for player in players { + let player_info = PlayerInfo { + profile: GameProfile { + uuid: player.uuid, + name: player.name.clone(), + properties: player.properties.clone(), + }, uuid: player.uuid, - name: player.name.clone(), - properties: player.properties.clone(), - }, - uuid: player.uuid, - gamemode: player.gamemode, - latency: player.latency, - display_name: player.display_name.clone(), - }; - players_lock.insert(player.uuid, player_info.clone()); - tx.send(Event::UpdatePlayers(UpdatePlayersEvent::Add(player_info))) - .unwrap(); + gamemode: player.gamemode, + latency: player.latency, + display_name: player.display_name.clone(), + }; + players_lock.insert(player.uuid, player_info.clone()); + events.push(Event::UpdatePlayers(UpdatePlayersEvent::Add( + player_info, + ))); + } } - } - Action::UpdateGameMode(players) => { - for player in players { - if let Some(p) = players_lock.get_mut(&player.uuid) { - p.gamemode = player.gamemode; - tx.send(Event::UpdatePlayers(UpdatePlayersEvent::GameMode { - uuid: player.uuid, - game_mode: player.gamemode, - })) - .unwrap(); - } else { - warn!( + Action::UpdateGameMode(players) => { + for player in players { + if let Some(p) = players_lock.get_mut(&player.uuid) { + p.gamemode = player.gamemode; + events.push(Event::UpdatePlayers( + UpdatePlayersEvent::GameMode { + uuid: player.uuid, + game_mode: player.gamemode, + }, + )); + } else { + warn!( "Ignoring PlayerInfo (UpdateGameMode) for unknown player {}", player.uuid ); + } } } - } - Action::UpdateLatency(players) => { - for player in players { - if let Some(p) = players_lock.get_mut(&player.uuid) { - p.latency = player.latency; - tx.send(Event::UpdatePlayers(UpdatePlayersEvent::Latency { - uuid: player.uuid, - latency: player.latency, - })) - .unwrap(); - } else { - warn!( - "Ignoring PlayerInfo (UpdateLatency) for unknown player {}", - player.uuid - ); + Action::UpdateLatency(players) => { + for player in players { + if let Some(p) = players_lock.get_mut(&player.uuid) { + p.latency = player.latency; + events.push(Event::UpdatePlayers( + UpdatePlayersEvent::Latency { + uuid: player.uuid, + latency: player.latency, + }, + )); + } else { + warn!( + "Ignoring PlayerInfo (UpdateLatency) for unknown player {}", + player.uuid + ); + } } } - } - Action::UpdateDisplayName(players) => { - for player in players { - if let Some(p) = players_lock.get_mut(&player.uuid) { - p.display_name = player.display_name.clone(); - tx.send(Event::UpdatePlayers(UpdatePlayersEvent::DisplayName { - uuid: player.uuid, - display_name: player.display_name.clone(), - })) - .unwrap(); - } else { - warn!( + Action::UpdateDisplayName(players) => { + for player in players { + if let Some(p) = players_lock.get_mut(&player.uuid) { + p.display_name = player.display_name.clone(); + events.push(Event::UpdatePlayers( + UpdatePlayersEvent::DisplayName { + uuid: player.uuid, + display_name: player.display_name.clone(), + }, + )); + } else { + warn!( "Ignoring PlayerInfo (UpdateDisplayName) for unknown player {}", player.uuid ); + } } } - } - Action::RemovePlayer(players) => { - for player in players { - if players_lock.remove(&player.uuid).is_some() { - tx.send(Event::UpdatePlayers(UpdatePlayersEvent::Remove { - uuid: player.uuid, - })) - .unwrap(); - } else { - warn!( - "Ignoring PlayerInfo (RemovePlayer) for unknown player {}", - player.uuid - ); + Action::RemovePlayer(players) => { + for player in players { + if players_lock.remove(&player.uuid).is_some() { + events.push(Event::UpdatePlayers(UpdatePlayersEvent::Remove { + uuid: player.uuid, + })); + } else { + warn!( + "Ignoring PlayerInfo (RemovePlayer) for unknown player {}", + player.uuid + ); + } } } } } - // TODO + for event in events { + tx.send(event).await?; + } } ClientboundGamePacket::SetChunkCacheCenter(p) => { debug!("Got chunk cache center packet {:?}", p); @@ -701,8 +765,29 @@ impl Client { .update_view_center(&ChunkPos::new(p.x, p.z)); } ClientboundGamePacket::LevelChunkWithLight(p) => { - debug!("Got chunk with light packet {} {}", p.x, p.z); + // debug!("Got chunk with light packet {} {}", p.x, p.z); let pos = ChunkPos::new(p.x, p.z); + + // OPTIMIZATION: if we already know about the chunk from the + // shared world (and not ourselves), then we don't need to + // parse it again. This is only used when we have a shared + // world, since we check that the chunk isn't currently owned + // by this client. + let shared_has_chunk = client.world.read().get_chunk(&pos).is_some(); + let this_client_has_chunk = client + .world + .read() + .chunk_storage + .limited_get(&pos) + .is_some(); + if shared_has_chunk && !this_client_has_chunk { + trace!( + "Skipping parsing chunk {:?} because we already know about it", + pos + ); + return Ok(()); + } + // let chunk = Chunk::read_with_world_height(&mut p.chunk_data); // debug("chunk {:?}") if let Err(e) = client @@ -727,7 +812,7 @@ impl Client { if let Some(mut entity) = world.entity_mut(p.id) { entity.apply_metadata(&p.packed_items.0); } else { - warn!("Server sent an entity data packet for an entity id ({}) that we don't know about", p.id); + // warn!("Server sent an entity data packet for an entity id ({}) that we don't know about", p.id); } } ClientboundGamePacket::UpdateAttributes(_p) => { @@ -759,10 +844,11 @@ impl Client { ClientboundGamePacket::SetHealth(p) => { debug!("Got set health packet {:?}", p); if p.health == 0.0 { - let mut dead_lock = client.dead.lock(); - if !*dead_lock { - *dead_lock = true; - tx.send(Event::Death(None)).unwrap(); + // we can't define a variable here with client.dead.lock() + // because of https://github.com/rust-lang/rust/issues/57478 + if !*client.dead.lock() { + *client.dead.lock() = true; + tx.send(Event::Death(None)).await?; } } } @@ -771,17 +857,14 @@ impl Client { } ClientboundGamePacket::TeleportEntity(p) => { let mut world_lock = client.world.write(); - - world_lock - .set_entity_pos( - p.id, - Vec3 { - x: p.x, - y: p.y, - z: p.z, - }, - ) - .map_err(|e| HandleError::Other(e.into()))?; + let _ = world_lock.set_entity_pos( + p.id, + Vec3 { + x: p.x, + y: p.y, + z: p.z, + }, + ); } ClientboundGamePacket::UpdateAdvancements(p) => { debug!("Got update advancements packet {:?}", p); @@ -792,16 +875,12 @@ impl Client { ClientboundGamePacket::MoveEntityPos(p) => { let mut world_lock = client.world.write(); - world_lock - .move_entity_with_delta(p.entity_id, &p.delta) - .map_err(|e| HandleError::Other(e.into()))?; + let _ = world_lock.move_entity_with_delta(p.entity_id, &p.delta); } ClientboundGamePacket::MoveEntityPosRot(p) => { let mut world_lock = client.world.write(); - world_lock - .move_entity_with_delta(p.entity_id, &p.delta) - .map_err(|e| HandleError::Other(e.into()))?; + let _ = world_lock.move_entity_with_delta(p.entity_id, &p.delta); } ClientboundGamePacket::MoveEntityRot(_p) => { // debug!("Got move entity rot packet {:?}", p); @@ -816,16 +895,16 @@ impl Client { debug!("Got remove entities packet {:?}", p); } ClientboundGamePacket::PlayerChat(p) => { - // debug!("Got player chat packet {:?}", p); + debug!("Got player chat packet {:?}", p); tx.send(Event::Chat(ChatPacket::Player(Box::new(p.clone())))) - .unwrap(); + .await?; } ClientboundGamePacket::SystemChat(p) => { debug!("Got system chat packet {:?}", p); - tx.send(Event::Chat(ChatPacket::System(p.clone()))).unwrap(); + tx.send(Event::Chat(ChatPacket::System(p.clone()))).await?; } - ClientboundGamePacket::Sound(p) => { - debug!("Got sound packet {:?}", p); + ClientboundGamePacket::Sound(_p) => { + // debug!("Got sound packet {:?}", p); } ClientboundGamePacket::LevelEvent(p) => { debug!("Got level event packet {:?}", p); @@ -892,10 +971,11 @@ impl Client { ClientboundGamePacket::PlayerCombatKill(p) => { debug!("Got player kill packet {:?}", p); if *client.entity_id.read() == p.player_id { - let mut dead_lock = client.dead.lock(); - if !*dead_lock { - *dead_lock = true; - tx.send(Event::Death(Some(Box::new(p.clone())))).unwrap(); + // we can't define a variable here with client.dead.lock() + // because of https://github.com/rust-lang/rust/issues/57478 + if !*client.dead.lock() { + *client.dead.lock() = true; + tx.send(Event::Death(Some(Box::new(p.clone())))).await?; } } } @@ -938,7 +1018,7 @@ impl Client { } /// Runs game_tick every 50 milliseconds. - async fn game_tick_loop(mut client: Client, tx: UnboundedSender) { + async fn game_tick_loop(mut client: Client, tx: Sender) { let mut game_tick_interval = time::interval(time::Duration::from_millis(50)); // TODO: Minecraft bursts up to 10 ticks and then skips, we should too game_tick_interval.set_missed_tick_behavior(time::MissedTickBehavior::Burst); @@ -949,24 +1029,25 @@ impl Client { } /// Runs every 50 milliseconds. - async fn game_tick(client: &mut Client, tx: &UnboundedSender) { + async fn game_tick(client: &mut Client, tx: &Sender) { // return if there's no chunk at the player's position + { - let world_lock = client.world.write(); + let world_lock = client.world.read(); let player_entity_id = *client.entity_id.read(); let player_entity = world_lock.entity(player_entity_id); - let player_entity = if let Some(player_entity) = player_entity { - player_entity - } else { + let Some(player_entity) = player_entity else { return; }; let player_chunk_pos: ChunkPos = player_entity.pos().into(); - if world_lock[&player_chunk_pos].is_none() { + if world_lock.get_chunk(&player_chunk_pos).is_none() { return; } } - tx.send(Event::Tick).unwrap(); + tx.send(Event::Tick) + .await + .expect("Sending tick event should never fail"); // TODO: if we're a passenger, send the required packets @@ -978,15 +1059,34 @@ impl Client { // TODO: minecraft does ambient sounds here } + /// Get a [`WeakWorld`] from our world container. If it's a normal client, + /// then it'll be the same as the world the client has loaded. If the + /// client using a shared world, then the shared world will be a superset + /// of the client's world. + /// + /// # Panics + /// Panics if the client has not received the login packet yet. You can check this with [`Client::logged_in`]. + pub fn world(&self) -> Arc { + let world_name = self.world_name.read(); + let world_name = world_name + .as_ref() + .expect("Client has not received login packet yet"); + if let Some(world) = self.world_container.read().get(world_name) { + world + } else { + unreachable!("The world name must be in the world container"); + } + } + /// Returns the entity associated to the player. pub fn entity_mut(&self) -> Entity> { let entity_id = *self.entity_id.read(); - let mut world = self.world.write(); + let world = self.world.write(); let entity_data = world .entity_storage - .get_mut_by_id(entity_id) + .get_by_id(entity_id) .expect("Player entity should exist"); let entity_ptr = unsafe { entity_data.as_ptr() }; Entity::new(world, entity_id, entity_ptr) @@ -994,26 +1094,36 @@ impl Client { /// Returns the entity associated to the player. pub fn entity(&self) -> Entity> { let entity_id = *self.entity_id.read(); - let world = self.world.read(); let entity_data = world .entity_storage .get_by_id(entity_id) .expect("Player entity should be in the given world"); - let entity_ptr = unsafe { entity_data.as_const_ptr() }; + let entity_ptr = unsafe { entity_data.as_ptr() }; Entity::new(world, entity_id, entity_ptr) } /// Returns whether we have a received the login packet yet. pub fn logged_in(&self) -> bool { - let world = self.world.read(); - let entity_id = *self.entity_id.read(); - world.entity(entity_id).is_some() + // the login packet tells us the world name + self.world_name.read().is_some() } /// Tell the server we changed our game options (i.e. render distance, main hand). /// If this is not set before the login packet, the default will be sent. + /// + /// ```rust,no_run + /// # use azalea_client::{Client, ClientInformation}; + /// # async fn example(bot: Client) -> Result<(), Box> { + /// bot.set_client_information(ClientInformation { + /// view_distance: 2, + /// ..Default::default() + /// }) + /// .await?; + /// # Ok(()) + /// # } + /// ``` pub async fn set_client_information( &self, client_information: ServerboundClientInformationPacket, diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs old mode 100755 new mode 100644 index ebcc4477..91c8cd91 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -7,6 +7,8 @@ #![allow(incomplete_features)] #![feature(trait_upcasting)] +#![feature(error_generic_member_access)] +#![feature(provide_any)] mod account; mod chat; @@ -18,10 +20,10 @@ mod player; mod plugins; pub use account::Account; -pub use client::{ChatPacket, Client, ClientInformation, Event, JoinError}; +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 { diff --git a/azalea-client/src/movement.rs b/azalea-client/src/movement.rs old mode 100755 new mode 100644 index 87ac8d85..5fca924b --- a/azalea-client/src/movement.rs +++ b/azalea-client/src/movement.rs @@ -1,3 +1,5 @@ +use std::backtrace::Backtrace; + use crate::Client; use azalea_core::Vec3; use azalea_physics::collision::{MovableEntity, MoverType}; @@ -15,7 +17,7 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum MovePlayerError { #[error("Player is not in world")] - PlayerNotInWorld, + PlayerNotInWorld(Backtrace), #[error("{0}")] Io(#[from] std::io::Error), } @@ -23,7 +25,9 @@ pub enum MovePlayerError { impl From for MovePlayerError { fn from(err: MoveEntityError) -> Self { match err { - MoveEntityError::EntityDoesNotExist => MovePlayerError::PlayerNotInWorld, + MoveEntityError::EntityDoesNotExist(backtrace) => { + MovePlayerError::PlayerNotInWorld(backtrace) + } } } } @@ -152,7 +156,7 @@ impl Client { } // Set our current position to the provided Vec3, potentially clipping through blocks. - pub async fn set_pos(&mut self, new_pos: Vec3) -> Result<(), MovePlayerError> { + pub async fn set_position(&mut self, new_pos: Vec3) -> Result<(), MovePlayerError> { let player_entity_id = *self.entity_id.read(); let mut world_lock = self.world.write(); @@ -167,7 +171,7 @@ impl Client { let mut entity = world_lock .entity_mut(player_entity_id) - .ok_or(MovePlayerError::PlayerNotInWorld)?; + .ok_or(MovePlayerError::PlayerNotInWorld(Backtrace::capture()))?; log::trace!( "move entity bounding box: {} {:?}", entity.id, @@ -258,6 +262,19 @@ impl Client { /// Start walking in the given direction. To sprint, use /// [`Client::sprint`]. To stop walking, call walk with /// `WalkDirection::None`. + /// + /// # Examples + /// + /// Walk for 1 second + /// ```rust,no_run + /// # use azalea_client::{Client, WalkDirection}; + /// # use std::time::Duration; + /// # async fn example(mut bot: Client) { + /// bot.walk(WalkDirection::Forward); + /// tokio::time::sleep(Duration::from_secs(1)).await; + /// bot.walk(WalkDirection::None); + /// # } + /// ``` pub fn walk(&mut self, direction: WalkDirection) { { let mut physics_state = self.physics_state.lock(); @@ -269,6 +286,19 @@ impl Client { /// Start sprinting in the given direction. To stop moving, call /// [`Client::walk(WalkDirection::None)`] + /// + /// # Examples + /// + /// Sprint for 1 second + /// ```rust,no_run + /// # use azalea_client::{Client, WalkDirection, SprintDirection}; + /// # use std::time::Duration; + /// # async fn example(mut bot: Client) { + /// bot.sprint(SprintDirection::Forward); + /// tokio::time::sleep(Duration::from_secs(1)).await; + /// bot.walk(WalkDirection::None); + /// # } + /// ``` pub fn sprint(&mut self, direction: SprintDirection) { let mut physics_state = self.physics_state.lock(); physics_state.move_direction = WalkDirection::from(direction); @@ -321,6 +351,7 @@ impl Client { /// Sets your rotation. `y_rot` is yaw (looking to the side), `x_rot` is /// pitch (looking up and down). You can get these numbers from the vanilla /// f3 screen. + /// `y_rot` goes from -180 to 180, and `x_rot` goes from -90 to 90. pub fn set_rotation(&mut self, y_rot: f32, x_rot: f32) { let mut player_entity = self.entity_mut(); player_entity.set_rotation(y_rot, x_rot); diff --git a/azalea-client/src/player.rs b/azalea-client/src/player.rs index 5db5c864..1b4f052b 100755 --- a/azalea-client/src/player.rs +++ b/azalea-client/src/player.rs @@ -1,7 +1,6 @@ use azalea_auth::game_profile::GameProfile; use azalea_chat::Component; use azalea_core::GameType; -use azalea_world::entity::EntityData; use azalea_world::World; use uuid::Uuid; @@ -21,19 +20,3 @@ pub struct PlayerInfo { /// The player's display name in the tab list. pub display_name: Option, } - -impl PlayerInfo { - /// Get a reference to the entity of the player in the world. - pub fn entity<'d>(&'d self, world: &'d World) -> Option<&EntityData> { - world.entity_by_uuid(&self.uuid) - } - - /// Get a mutable reference to the entity of the player in the world. - pub fn entity_mut<'d>(&'d mut self, world: &'d mut World) -> Option<&'d mut EntityData> { - world.entity_mut_by_uuid(&self.uuid) - } - - pub fn set_uuid(&mut self, uuid: Uuid) { - self.uuid = uuid; - } -} diff --git a/azalea-client/src/plugins.rs b/azalea-client/src/plugins.rs index 150d5960..93641906 100644 --- a/azalea-client/src/plugins.rs +++ b/azalea-client/src/plugins.rs @@ -10,31 +10,24 @@ use std::{ type U64Hasher = 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, Default)] -pub struct Plugins { - map: Option, U64Hasher>>, +pub struct PluginStates { + map: Option, U64Hasher>>, } -impl Plugins { - pub fn new() -> Self { - Self::default() - } +/// A map of PluginState TypeIds to AnyPlugin objects. This can then be built +/// into a [`PluginStates`] object to get a fresh new state based on this +/// plugin. +/// +/// If you're using the azalea crate, you should generate this from the +/// `plugins!` macro. +#[derive(Clone, Default)] +pub struct Plugins { + map: Option, U64Hasher>>, +} - pub fn add(&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::(), Box::new(plugin)); - } - - pub fn get(&self) -> Option<&T> { +impl PluginStates { + pub fn get(&self) -> Option<&T> { self.map .as_ref() .and_then(|map| map.get(&TypeId::of::())) @@ -42,10 +35,40 @@ impl Plugins { } } -impl IntoIterator for Plugins { - type Item = Box; +impl Plugins { + /// Create a new empty set of plugins. + pub fn new() -> Self { + Self::default() + } + + /// Add a new plugin to this set. + pub fn add(&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::(), Box::new(plugin)); + } + + /// Build our plugin states from this set of plugins. Note that if you're + /// using `azalea` you'll probably never need to use this as it's called + /// for you. + 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 PluginStates { + type Item = Box; type IntoIter = std::vec::IntoIter; + /// Iterate over the plugin states. fn into_iter(self) -> Self::IntoIter { self.map .map(|map| map.into_values().collect::>()) @@ -54,26 +77,67 @@ impl IntoIterator for Plugins { } } -/// Plugins can keep their own personal state, listen to events, and add new functions to Client. +/// A `PluginState` keeps the current state of a plugin for a client. All the +/// fields must be atomic. Unique `PluginState`s are built from [`Plugin`]s. #[async_trait] -pub trait Plugin: Send + Sync + PluginClone + Any + 'static { +pub trait PluginState: Send + Sync + PluginStateClone + Any + 'static { async fn handle(self: Box, event: Event, bot: Client); } -/// An internal trait that allows Plugin to be cloned. -#[doc(hidden)] -pub trait PluginClone { - fn clone_box(&self) -> Box; +/// Plugins can keep their own personal state, listen to [`Event`]s, and add +/// new functions to [`Client`]. +pub trait Plugin: Send + Sync + Any + 'static { + type State: PluginState; + + fn build(&self) -> Self::State; } -impl PluginClone for T + +/// AnyPlugin is basically a Plugin but without the State associated type +/// it has to exist so we can do a hashmap with Box +#[doc(hidden)] +pub trait AnyPlugin: Send + Sync + Any + AnyPluginClone + 'static { + fn build(&self) -> Box; +} + +impl + Clone> AnyPlugin for B { + fn build(&self) -> Box { + Box::new(self.build()) + } +} + +/// An internal trait that allows PluginState to be cloned. +#[doc(hidden)] +pub trait PluginStateClone { + fn clone_box(&self) -> Box; +} +impl PluginStateClone for T where - T: 'static + Plugin + Clone, + T: 'static + PluginState + Clone, { - fn clone_box(&self) -> Box { + fn clone_box(&self) -> Box { Box::new(self.clone()) } } -impl Clone for Box { +impl Clone for Box { + fn clone(&self) -> Self { + self.clone_box() + } +} + +/// An internal trait that allows AnyPlugin to be cloned. +#[doc(hidden)] +pub trait AnyPluginClone { + fn clone_box(&self) -> Box; +} +impl AnyPluginClone for T +where + T: 'static + Plugin + Clone, +{ + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} +impl Clone for Box { fn clone(&self) -> Self { self.clone_box() } diff --git a/azalea-core/src/direction.rs b/azalea-core/src/direction.rs index 5a7f601a..95dacc69 100755 --- a/azalea-core/src/direction.rs +++ b/azalea-core/src/direction.rs @@ -1,7 +1,5 @@ use azalea_buf::McBuf; -use crate::floor_mod; - #[derive(Clone, Copy, Debug, McBuf, Default)] pub enum Direction { #[default] @@ -116,7 +114,7 @@ impl AxisCycle { } } pub fn between(axis0: Axis, axis1: Axis) -> Self { - Self::from_ordinal(floor_mod(axis1 as i32 - axis0 as i32, 3)) + Self::from_ordinal(i32::rem_euclid(axis1 as i32 - axis0 as i32, 3) as u32) } pub fn inverse(self) -> Self { match self { @@ -128,8 +126,8 @@ impl AxisCycle { pub fn cycle(self, axis: Axis) -> Axis { match self { Self::None => axis, - Self::Forward => Axis::from_ordinal(floor_mod(axis as i32 + 1, 3)), - Self::Backward => Axis::from_ordinal(floor_mod(axis as i32 - 1, 3)), + Self::Forward => Axis::from_ordinal(i32::rem_euclid(axis as i32 + 1, 3) as u32), + Self::Backward => Axis::from_ordinal(i32::rem_euclid(axis as i32 - 1, 3) as u32), } } pub fn cycle_xyz(self, x: i32, y: i32, z: i32, axis: Axis) -> i32 { diff --git a/azalea-core/src/lib.rs b/azalea-core/src/lib.rs index f7726a38..7c74bdcb 100755 --- a/azalea-core/src/lib.rs +++ b/azalea-core/src/lib.rs @@ -38,16 +38,6 @@ pub use aabb::*; mod block_hit_result; pub use block_hit_result::*; -// java moment -// TODO: add tests and optimize/simplify this -pub fn floor_mod(x: i32, y: u32) -> u32 { - if x < 0 { - y - ((-x) as u32 % y) - } else { - x as u32 % y - } -} - // TODO: make this generic pub fn binary_search(mut min: i32, max: i32, predicate: &dyn Fn(i32) -> bool) -> i32 { let mut diff = max - min; diff --git a/azalea-crypto/src/signing.rs b/azalea-crypto/src/signing.rs index 7df0963b..1753eec2 100755 --- a/azalea-crypto/src/signing.rs +++ b/azalea-crypto/src/signing.rs @@ -7,12 +7,12 @@ pub struct SaltSignaturePair { pub signature: Vec, } -#[derive(Clone, Debug, Default, McBuf)] +#[derive(Clone, Debug, Default, McBuf, PartialEq)] pub struct MessageSignature { pub bytes: Vec, } -#[derive(Clone, Debug, McBuf)] +#[derive(Clone, Debug, McBuf, PartialEq)] pub struct SignedMessageHeader { pub previous_signature: Option, pub sender: Uuid, diff --git a/azalea-physics/Cargo.toml b/azalea-physics/Cargo.toml old mode 100755 new mode 100644 diff --git a/azalea-physics/src/collision/mod.rs b/azalea-physics/src/collision/mod.rs old mode 100755 new mode 100644 diff --git a/azalea-physics/src/collision/world_collisions.rs b/azalea-physics/src/collision/world_collisions.rs index 65f7f5bb..a4062fcc 100644 --- a/azalea-physics/src/collision/world_collisions.rs +++ b/azalea-physics/src/collision/world_collisions.rs @@ -57,7 +57,7 @@ impl<'a> BlockCollisions<'a> { } } - fn get_chunk(&self, block_x: i32, block_z: i32) -> Option<&Arc>> { + fn get_chunk(&self, block_x: i32, block_z: i32) -> Option>> { let chunk_x = ChunkSectionPos::block_to_section_coord(block_x); let chunk_z = ChunkSectionPos::block_to_section_coord(block_z); let chunk_pos = ChunkPos::new(chunk_x, chunk_z); @@ -75,7 +75,7 @@ impl<'a> BlockCollisions<'a> { // return var7; // } - self.world[&chunk_pos].as_ref() + self.world.get_chunk(&chunk_pos) } } diff --git a/azalea-physics/src/lib.rs b/azalea-physics/src/lib.rs old mode 100755 new mode 100644 index 2295e6f2..34d31a0e --- a/azalea-physics/src/lib.rs +++ b/azalea-physics/src/lib.rs @@ -231,7 +231,10 @@ fn jump_boost_power>(_entity: &Entity) -> f64 { mod tests { use super::*; use azalea_core::ChunkPos; - use azalea_world::{Chunk, World}; + use azalea_world::{ + entity::{metadata, EntityMetadata}, + Chunk, World, + }; use uuid::Uuid; #[test] @@ -247,6 +250,7 @@ mod tests { y: 70., z: 0., }, + EntityMetadata::Player(metadata::Player::default()), ), ); let mut entity = world.entity_mut(0).unwrap(); @@ -279,6 +283,7 @@ mod tests { y: 70., z: 0.5, }, + EntityMetadata::Player(metadata::Player::default()), ), ); let block_state = world.set_block_state(&BlockPos { x: 0, y: 69, z: 0 }, BlockState::Stone); @@ -311,6 +316,7 @@ mod tests { y: 71., z: 0.5, }, + EntityMetadata::Player(metadata::Player::default()), ), ); let block_state = world.set_block_state( @@ -344,6 +350,7 @@ mod tests { y: 71., z: 0.5, }, + EntityMetadata::Player(metadata::Player::default()), ), ); let block_state = world.set_block_state( @@ -377,6 +384,7 @@ mod tests { y: 73., z: 0.5, }, + EntityMetadata::Player(metadata::Player::default()), ), ); let block_state = world.set_block_state( diff --git a/azalea-protocol/src/connect.rs b/azalea-protocol/src/connect.rs index 00685d3c..567e4c40 100755 --- a/azalea-protocol/src/connect.rs +++ b/azalea-protocol/src/connect.rs @@ -44,8 +44,22 @@ pub struct WriteConnection { /// /// Join an offline-mode server and go through the handshake. /// ```rust,no_run +/// use azalea_protocol::{ +/// resolver, +/// connect::Connection, +/// packets::{ +/// ConnectionProtocol, PROTOCOL_VERSION, +/// login::{ +/// ClientboundLoginPacket, +/// serverbound_hello_packet::ServerboundHelloPacket, +/// serverbound_key_packet::{ServerboundKeyPacket, NonceOrSaltSignature} +/// }, +/// handshake::client_intention_packet::ClientIntentionPacket +/// } +/// }; +/// /// #[tokio::main] -/// async fn main() -> anyhow::Result<()> { +/// async fn main() -> Result<(), Box> { /// let resolved_address = resolver::resolve_address(&"localhost".try_into().unwrap()).await?; /// let mut conn = Connection::new(&resolved_address).await?; /// @@ -97,8 +111,8 @@ pub struct WriteConnection { /// break (conn.game(), p.game_profile); /// } /// ClientboundLoginPacket::LoginDisconnect(p) => { -/// println!("login disconnect: {}", p.reason); -/// bail!("{}", p.reason); +/// eprintln!("login disconnect: {}", p.reason); +/// return Err("login disconnect".into()); /// } /// ClientboundLoginPacket::CustomQuery(p) => {} /// } @@ -258,24 +272,51 @@ impl Connection { /// # Examples /// /// ```rust,no_run - /// let token = azalea_auth::auth(azalea_auth::AuthOpts { - /// ..Default::default() - /// }) - /// .await; - /// let player_data = azalea_auth::get_profile(token).await; + /// use azalea_auth::AuthResult; + /// use azalea_protocol::connect::Connection; + /// use azalea_protocol::packets::login::{ + /// ClientboundLoginPacket, + /// serverbound_key_packet::{ServerboundKeyPacket, NonceOrSaltSignature} + /// }; + /// use uuid::Uuid; + /// # use azalea_protocol::ServerAddress; /// - /// let mut connection = azalea::Connection::new(&server_address).await?; + /// # async fn example() -> Result<(), Box> { + /// let AuthResult { access_token, profile } = azalea_auth::auth( + /// "example@example.com", + /// azalea_auth::AuthOpts::default() + /// ).await.expect("Couldn't authenticate"); + /// # + /// # let address = ServerAddress::try_from("example@example.com").unwrap(); + /// # let resolved_address = azalea_protocol::resolver::resolve_address(&address).await?; + /// + /// let mut conn = Connection::new(&resolved_address).await?; /// /// // transition to the login state, in a real program we would have done a handshake first - /// connection.login(); + /// let mut conn = conn.login(); /// - /// match connection.read().await? { - /// ClientboundLoginPacket::Hello(p) => { - /// // tell Mojang we're joining the server - /// connection.authenticate(&token, player_data.uuid, p).await?; - /// } - /// _ => {} + /// match conn.read().await? { + /// ClientboundLoginPacket::Hello(p) => { + /// // tell Mojang we're joining the server & enable encryption + /// let e = azalea_crypto::encrypt(&p.public_key, &p.nonce).unwrap(); + /// conn.authenticate( + /// &access_token, + /// &Uuid::parse_str(&profile.id).expect("Invalid UUID"), + /// e.secret_key, + /// p + /// ).await?; + /// conn.write( + /// ServerboundKeyPacket { + /// nonce_or_salt_signature: NonceOrSaltSignature::Nonce(e.encrypted_nonce), + /// key_bytes: e.encrypted_public_key, + /// }.get() + /// ).await?; + /// conn.set_encryption_key(e.secret_key); + /// } + /// _ => {} /// } + /// # Ok(()) + /// # } /// ``` pub async fn authenticate( &self, diff --git a/azalea-protocol/src/lib.rs b/azalea-protocol/src/lib.rs old mode 100755 new mode 100644 index 0fae75b1..052e740f --- a/azalea-protocol/src/lib.rs +++ b/azalea-protocol/src/lib.rs @@ -13,7 +13,7 @@ #![feature(error_generic_member_access)] #![feature(provide_any)] -use std::str::FromStr; +use std::{net::SocketAddr, str::FromStr}; #[cfg(feature = "connecting")] pub mod connect; @@ -35,13 +35,12 @@ pub mod write; /// assert_eq!(addr.host, "localhost"); /// assert_eq!(addr.port, 25565); /// ``` -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ServerAddress { pub host: String, pub port: u16, } -// impl try_from for ServerAddress impl<'a> TryFrom<&'a str> for ServerAddress { type Error = String; @@ -59,6 +58,18 @@ impl<'a> TryFrom<&'a str> for ServerAddress { } } +impl From for ServerAddress { + /// Convert an existing SocketAddr into a ServerAddress. This just converts + /// the ip to a string and passes along the port. The resolver will realize + /// it's already an IP address and not do any DNS requests. + fn from(addr: SocketAddr) -> Self { + ServerAddress { + host: addr.ip().to_string(), + port: addr.port(), + } + } +} + #[cfg(test)] mod tests { use std::io::Cursor; diff --git a/azalea-protocol/src/packets/game/clientbound_player_chat_packet.rs b/azalea-protocol/src/packets/game/clientbound_player_chat_packet.rs index fedc81df..0e271e3d 100755 --- a/azalea-protocol/src/packets/game/clientbound_player_chat_packet.rs +++ b/azalea-protocol/src/packets/game/clientbound_player_chat_packet.rs @@ -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, } -#[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, } -#[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, } -#[derive(Clone, Debug, McBuf)] +#[derive(Clone, Debug, McBuf, PartialEq)] pub enum FilterMask { PassThrough, FullyFiltered, diff --git a/azalea-protocol/src/packets/game/clientbound_system_chat_packet.rs b/azalea-protocol/src/packets/game/clientbound_system_chat_packet.rs index a3319721..9fe03fb2 100755 --- a/azalea-protocol/src/packets/game/clientbound_system_chat_packet.rs +++ b/azalea-protocol/src/packets/game/clientbound_system_chat_packet.rs @@ -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, diff --git a/azalea-world/Cargo.toml b/azalea-world/Cargo.toml old mode 100755 new mode 100644 diff --git a/azalea-world/src/chunk_storage.rs b/azalea-world/src/chunk_storage.rs index a03cbe7b..6a8a995e 100755 --- a/azalea-world/src/chunk_storage.rs +++ b/azalea-world/src/chunk_storage.rs @@ -4,36 +4,61 @@ use crate::World; use azalea_block::BlockState; use azalea_buf::BufReadError; use azalea_buf::{McBufReadable, McBufWritable}; -use azalea_core::floor_mod; use azalea_core::{BlockPos, ChunkBlockPos, ChunkPos, ChunkSectionBlockPos}; use log::debug; use log::trace; +use log::warn; use parking_lot::Mutex; +use parking_lot::RwLock; +use std::collections::HashMap; use std::fmt::Debug; use std::io::Cursor; -use std::{ - io::Write, - ops::{Index, IndexMut}, - sync::Arc, -}; +use std::sync::Weak; +use std::{io::Write, sync::Arc}; const SECTION_HEIGHT: u32 = 16; -pub struct ChunkStorage { +/// An efficient storage of chunks for a client that has a limited render +/// distance. This has support for using a shared [`WeakChunkStorage`]. If you +/// have an infinite render distance (like a server), you should use +/// [`ChunkStorage`] instead. +pub struct PartialChunkStorage { + /// Chunk storage that can be shared by clients. + shared: Arc>, + pub view_center: ChunkPos, chunk_radius: u32, view_range: u32, - pub height: u32, - pub min_y: i32, // chunks is a list of size chunk_radius * chunk_radius chunks: Vec>>>, } +/// A storage for chunks where they're only stored weakly, so if they're not +/// actively being used somewhere else they'll be forgotten. This is used for +/// shared worlds. +pub struct WeakChunkStorage { + pub height: u32, + pub min_y: i32, + pub chunks: HashMap>>, +} + +/// A storage of potentially infinite chunks in a world. Chunks are stored as +/// an `Arc` so they can be shared across threads. +pub struct ChunkStorage { + pub height: u32, + pub min_y: i32, + pub chunks: HashMap>>, +} + +/// A single chunk in a world (16*?*16 blocks). This only contains the blocks and biomes. You +/// can derive the height of the chunk from the number of sections, but you +/// need a [`ChunkStorage`] to get the minimum Y coordinate. #[derive(Debug)] pub struct Chunk { pub sections: Vec
, } +/// A section of a chunk, i.e. a 16*16*16 block area. #[derive(Clone, Debug)] pub struct Section { pub block_count: u16, @@ -59,22 +84,28 @@ impl Default for Chunk { } } -impl ChunkStorage { - pub fn new(chunk_radius: u32, height: u32, min_y: i32) -> Self { +impl PartialChunkStorage { + pub fn new(chunk_radius: u32, shared: Arc>) -> Self { let view_range = chunk_radius * 2 + 1; - ChunkStorage { + PartialChunkStorage { + shared, view_center: ChunkPos::new(0, 0), chunk_radius, view_range, - height, - min_y, chunks: vec![None; (view_range * view_range) as usize], } } + pub fn min_y(&self) -> i32 { + self.shared.read().min_y + } + pub fn height(&self) -> u32 { + self.shared.read().height + } + fn get_index(&self, chunk_pos: &ChunkPos) -> usize { - (floor_mod(chunk_pos.x, self.view_range) * self.view_range - + floor_mod(chunk_pos.z, self.view_range)) as usize + (i32::rem_euclid(chunk_pos.x, self.view_range as i32) * (self.view_range as i32) + + i32::rem_euclid(chunk_pos.z, self.view_range as i32)) as usize } pub fn in_range(&self, chunk_pos: &ChunkPos) -> bool { @@ -84,19 +115,19 @@ impl ChunkStorage { pub fn get_block_state(&self, pos: &BlockPos) -> Option { let chunk_pos = ChunkPos::from(pos); - let chunk = self[&chunk_pos].as_ref()?; + let chunk = self.get(&chunk_pos)?; let chunk = chunk.lock(); - chunk.get(&ChunkBlockPos::from(pos), self.min_y) + chunk.get(&ChunkBlockPos::from(pos), self.min_y()) } pub fn set_block_state(&self, pos: &BlockPos, state: BlockState) -> Option { - if pos.y < self.min_y || pos.y >= (self.min_y + self.height as i32) { + if pos.y < self.min_y() || pos.y >= (self.min_y() + self.height() as i32) { return None; } let chunk_pos = ChunkPos::from(pos); - let chunk = self[&chunk_pos].as_ref()?; + let chunk = self.get(&chunk_pos)?; let mut chunk = chunk.lock(); - Some(chunk.get_and_set(&ChunkBlockPos::from(pos), state, self.min_y)) + Some(chunk.get_and_set(&ChunkBlockPos::from(pos), state, self.min_y())) } pub fn replace_with_packet_data( @@ -116,27 +147,77 @@ impl ChunkStorage { let chunk = Arc::new(Mutex::new(Chunk::read_with_dimension_height( data, - self.height, + self.height(), )?)); trace!("Loaded chunk {:?}", pos); - self[pos] = Some(chunk); + self.set(pos, Some(chunk)); Ok(()) } -} -impl Index<&ChunkPos> for ChunkStorage { - type Output = Option>>; + /// Get a [`Chunk`] within render distance, or `None` if it's not loaded. + /// Use [`PartialChunkStorage::get`] to get a chunk from the shared storage. + pub fn limited_get(&self, pos: &ChunkPos) -> Option<&Arc>> { + if !self.in_range(pos) { + warn!( + "Chunk at {:?} is not in the render distance (center: {:?}, {} chunks)", + pos, self.view_center, self.chunk_radius, + ); + return None; + } - fn index(&self, pos: &ChunkPos) -> &Self::Output { - &self.chunks[self.get_index(pos)] + let index = self.get_index(pos); + self.chunks[index].as_ref() + } + /// Get a mutable reference to a [`Chunk`] within render distance, or + /// `None` if it's not loaded. Use [`PartialChunkStorage::get`] to get + /// a chunk from the shared storage. + pub fn limited_get_mut(&mut self, pos: &ChunkPos) -> Option<&mut Option>>> { + if !self.in_range(pos) { + return None; + } + + let index = self.get_index(pos); + Some(&mut self.chunks[index]) + } + + /// Get a chunk, + pub fn get(&self, pos: &ChunkPos) -> Option>> { + self.shared + .read() + .chunks + .get(pos) + .and_then(|chunk| chunk.upgrade()) + } + + /// Set a chunk in the shared storage and reference it from the limited + /// storage. + /// + /// # Panics + /// If the chunk is not in the render distance. + pub fn set(&mut self, pos: &ChunkPos, chunk: Option>>) { + if let Some(chunk) = &chunk { + self.shared + .write() + .chunks + .insert(*pos, Arc::downgrade(chunk)); + } else { + // don't remove it from the shared storage, since it'll be removed + // automatically if this was the last reference + } + if let Some(chunk_mut) = self.limited_get_mut(pos) { + *chunk_mut = chunk; + } } } -impl IndexMut<&ChunkPos> for ChunkStorage { - fn index_mut<'a>(&'a mut self, pos: &ChunkPos) -> &'a mut Self::Output { - let index = self.get_index(pos); - &mut self.chunks[index] +impl WeakChunkStorage { + pub fn new(height: u32, min_y: i32) -> Self { + WeakChunkStorage { + height, + min_y, + chunks: HashMap::new(), + } } } @@ -214,14 +295,14 @@ impl McBufWritable for Chunk { } } -impl Debug for ChunkStorage { +impl Debug for PartialChunkStorage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ChunkStorage") .field("view_center", &self.view_center) .field("chunk_radius", &self.chunk_radius) .field("view_range", &self.view_range) - .field("height", &self.height) - .field("min_y", &self.min_y) + .field("height", &self.height()) + .field("min_y", &self.min_y()) // .field("chunks", &self.chunks) .field("chunks", &format_args!("{} items", self.chunks.len())) .finish() @@ -292,9 +373,14 @@ impl Section { } } -impl Default for ChunkStorage { +impl Default for PartialChunkStorage { fn default() -> Self { - Self::new(8, 384, -64) + Self::new(8, Arc::new(RwLock::new(WeakChunkStorage::default()))) + } +} +impl Default for WeakChunkStorage { + fn default() -> Self { + Self::new(384, -64) } } @@ -317,8 +403,11 @@ mod tests { #[test] fn test_out_of_bounds_y() { - let mut chunk_storage = ChunkStorage::default(); - chunk_storage[&ChunkPos { x: 0, z: 0 }] = Some(Arc::new(Mutex::new(Chunk::default()))); + let mut chunk_storage = PartialChunkStorage::default(); + chunk_storage.set( + &ChunkPos { x: 0, z: 0 }, + Some(Arc::new(Mutex::new(Chunk::default()))), + ); assert!(chunk_storage .get_block_state(&BlockPos { x: 0, y: 319, z: 0 }) .is_some()); diff --git a/azalea-world/src/container.rs b/azalea-world/src/container.rs new file mode 100644 index 00000000..acdc9b05 --- /dev/null +++ b/azalea-world/src/container.rs @@ -0,0 +1,54 @@ +use crate::WeakWorld; +use azalea_core::ResourceLocation; +use log::error; +use std::{ + collections::HashMap, + sync::{Arc, Weak}, +}; + +/// A container of [`WeakWorld`]s. Worlds are stored as a Weak pointer here, so +/// if no clients are using a world it will be forgotten. +#[derive(Default)] +pub struct WeakWorldContainer { + pub worlds: HashMap>, +} + +impl WeakWorldContainer { + pub fn new() -> Self { + WeakWorldContainer { + worlds: HashMap::new(), + } + } + + /// Get a world from the container. + pub fn get(&self, name: &ResourceLocation) -> Option> { + self.worlds.get(name).and_then(|world| world.upgrade()) + } + + /// Add an empty world to the container (or not if it already exists) and + /// returns a strong reference to the world. + #[must_use = "the world will be immediately forgotten if unused"] + pub fn insert(&mut self, name: ResourceLocation, height: u32, min_y: i32) -> Arc { + if let Some(existing) = self.worlds.get(&name).and_then(|world| world.upgrade()) { + if existing.height() != height { + error!( + "Shared dimension height mismatch: {} != {}", + existing.height(), + height, + ); + } + if existing.min_y() != min_y { + error!( + "Shared world min_y mismatch: {} != {}", + existing.min_y(), + min_y, + ); + } + existing + } else { + let world = Arc::new(WeakWorld::new(height, min_y)); + self.worlds.insert(name, Arc::downgrade(&world)); + world + } + } +} diff --git a/azalea-world/src/entity/attributes.rs b/azalea-world/src/entity/attributes.rs index f7e9682e..fca6b88f 100644 --- a/azalea-world/src/entity/attributes.rs +++ b/azalea-world/src/entity/attributes.rs @@ -1,4 +1,4 @@ -//! https://minecraft.fandom.com/wiki/Attribute +//! use std::{ collections::HashMap, diff --git a/azalea-world/src/entity/mod.rs b/azalea-world/src/entity/mod.rs index 4611f215..dbf7e665 100644 --- a/azalea-world/src/entity/mod.rs +++ b/azalea-world/src/entity/mod.rs @@ -270,20 +270,11 @@ impl EntityData { &self.pos } - /// Convert this &mut self into a (mutable) pointer. - /// - /// # Safety - /// The entity MUST exist while this pointer exists. - pub unsafe fn as_ptr(&mut self) -> NonNull { - NonNull::new_unchecked(self as *mut EntityData) - } - /// Convert this &self into a (mutable) pointer. /// /// # Safety - /// The entity MUST exist while this pointer exists. You also must not - /// modify the data inside the pointer. - pub unsafe fn as_const_ptr(&self) -> NonNull { + /// The entity MUST exist for at least as long as this pointer exists. + pub unsafe fn as_ptr(&self) -> NonNull { // this is cursed NonNull::new_unchecked(self as *const EntityData as *mut EntityData) } diff --git a/azalea-world/src/entity_storage.rs b/azalea-world/src/entity_storage.rs index 02d7d55a..c8c58a75 100755 --- a/azalea-world/src/entity_storage.rs +++ b/azalea-world/src/entity_storage.rs @@ -2,101 +2,229 @@ use crate::entity::EntityData; use azalea_core::ChunkPos; use log::warn; use nohash_hasher::{IntMap, IntSet}; -use std::collections::HashMap; +use parking_lot::RwLock; +use std::{ + collections::HashMap, + sync::{Arc, Weak}, +}; use uuid::Uuid; -#[derive(Debug)] -pub struct EntityStorage { - data_by_id: IntMap, - id_by_chunk: HashMap>, - id_by_uuid: HashMap, +// How entity updates are processed (to avoid issues with shared worlds) +// - each bot contains a map of { entity id: updates received } +// - the shared world also contains a canonical "true" updates received for each entity +// - when a client loads an entity, its "updates received" is set to the same as the global "updates received" +// - when the shared world sees an entity for the first time, the "updates received" is set to 1. +// - clients can force the shared "updates received" to 0 to make it so certain entities (i.e. other bots in our swarm) don't get confused and updated by other bots +// - when a client gets an update to an entity, we check if our "updates received" is the same as the shared world's "updates received": +// if it is, then process the update and increment the client's and shared world's "updates received" +// if not, then we simply increment our local "updates received" and do nothing else + +/// Store a map of entities by ID. To get an iterator over all entities, use +/// `storage.shared.read().entities` [`WeakEntityStorage::entities`]. +/// +/// This is meant to be used with shared worlds. +#[derive(Debug, Default)] +pub struct PartialEntityStorage { + pub shared: Arc>, + + /// The entity id of the player that owns this struct. + pub owner_entity_id: u32, + pub updates_received: IntMap, + /// Strong references to the entities we have loaded. + data_by_id: IntMap>, } -impl EntityStorage { - pub fn new() -> Self { +/// Weakly store entities in a world. If the entities aren't being referenced +/// by anything else (like an [`PartialEntityStorage`]), they'll be forgotten. +#[derive(Debug, Default)] +pub struct WeakEntityStorage { + data_by_id: IntMap>, + /// An index of all the entity ids we know are in a chunk + ids_by_chunk: HashMap>, + /// An index of entity ids by their UUIDs + id_by_uuid: HashMap, + + pub updates_received: IntMap, +} + +impl PartialEntityStorage { + pub fn new(shared: Arc>, owner_entity_id: u32) -> Self { + shared.write().updates_received.insert(owner_entity_id, 0); Self { + shared, + owner_entity_id, + updates_received: IntMap::default(), data_by_id: IntMap::default(), - id_by_chunk: HashMap::default(), - id_by_uuid: HashMap::default(), } } /// Add an entity to the storage. #[inline] pub fn insert(&mut self, id: u32, entity: EntityData) { - self.id_by_chunk + // if the entity is already in the shared world, we don't need to do anything + if self.shared.read().data_by_id.contains_key(&id) { + return; + } + + // add the entity to the "indexes" + let mut shared = self.shared.write(); + shared + .ids_by_chunk .entry(ChunkPos::from(entity.pos())) .or_default() .insert(id); - self.id_by_uuid.insert(entity.uuid, id); + shared.id_by_uuid.insert(entity.uuid, id); + + // now store the actual entity data + let entity = Arc::new(entity); + shared.data_by_id.insert(id, Arc::downgrade(&entity)); self.data_by_id.insert(id, entity); + // set our updates_received to the shared updates_received, unless it's + // not there in which case set both to 1 + if let Some(&shared_updates_received) = shared.updates_received.get(&id) { + // 0 means we're never tracking updates for this entity + if shared_updates_received != 0 || id == self.owner_entity_id { + self.updates_received.insert(id, 1); + } + } else { + shared.updates_received.insert(id, 1); + self.updates_received.insert(id, 1); + } } - /// Remove an entity from the storage by its id. + /// Remove an entity from this storage by its id. It will only be removed + /// from the shared storage if there are no other references to it. #[inline] pub fn remove_by_id(&mut self, id: u32) { if let Some(entity) = self.data_by_id.remove(&id) { - let entity_chunk = ChunkPos::from(entity.pos()); - let entity_uuid = entity.uuid; - if self.id_by_chunk.remove(&entity_chunk).is_none() { - warn!("Tried to remove entity with id {id} from chunk {entity_chunk:?} but it was not found."); - } - if self.id_by_uuid.remove(&entity_uuid).is_none() { - warn!("Tried to remove entity with id {id} from uuid {entity_uuid:?} but it was not found."); - } + let chunk = ChunkPos::from(entity.pos()); + let uuid = entity.uuid; + self.updates_received.remove(&id); + drop(entity); + // maybe remove it from the storage + self.shared.write().remove_entity_if_unused(id, uuid, chunk); } else { warn!("Tried to remove entity with id {id} but it was not found.") } } - /// Check if there is an entity that exists with the given id. + /// Whether the entity with the given id is being loaded by this storage. + /// If you want to check whether the entity is in the shared storage, use + /// [`WeakEntityStorage::contains_id`]. #[inline] - pub fn contains_id(&self, id: &u32) -> bool { + pub fn limited_contains_id(&self, id: &u32) -> bool { self.data_by_id.contains_key(id) } - /// Get a reference to an entity by its id. + /// Whether the entity with the given id is in the shared storage (i.e. + /// it's possible we don't see the entity but something else in the shared + /// storage does). To check whether the entity is being loaded by this + /// storage, use [`PartialEntityStorage::limited_contains_id`]. #[inline] - pub fn get_by_id(&self, id: u32) -> Option<&EntityData> { + pub fn contains_id(&self, id: &u32) -> bool { + self.shared.read().data_by_id.contains_key(id) + } + + /// Get a reference to an entity by its id, if it's being loaded by this storage. + #[inline] + pub fn limited_get_by_id(&self, id: u32) -> Option<&Arc> { self.data_by_id.get(&id) } - /// Get a mutable reference to an entity by its id. + /// Get a mutable reference to an entity by its id, if it's being loaded by + /// this storage. #[inline] - pub fn get_mut_by_id(&mut self, id: u32) -> Option<&mut EntityData> { + pub fn limited_get_mut_by_id(&mut self, id: u32) -> Option<&mut Arc> { self.data_by_id.get_mut(&id) } - /// Get a reference to an entity by its uuid. + /// Returns whether we're allowed to update this entity (to prevent two clients in + /// a shared world updating it twice), and acknowleges that we WILL update + /// it if it's true. Don't call this unless you actually got an entity + /// update that all other clients within render distance will get too. + pub fn maybe_update(&mut self, id: u32) -> bool { + let this_client_updates_received = self.updates_received.get(&id).copied(); + let shared_updates_received = self.shared.read().updates_received.get(&id).copied(); + + let can_update = this_client_updates_received == shared_updates_received; + if can_update { + let new_updates_received = this_client_updates_received.unwrap_or(0) + 1; + self.updates_received.insert(id, new_updates_received); + self.shared + .write() + .updates_received + .insert(id, new_updates_received); + true + } else { + false + } + } + + /// Get an entity in the shared storage by its id, if it exists. #[inline] - pub fn get_by_uuid(&self, uuid: &Uuid) -> Option<&EntityData> { - self.id_by_uuid + pub fn get_by_id(&self, id: u32) -> Option> { + self.shared + .read() + .data_by_id + .get(&id) + .and_then(|e| e.upgrade()) + } + + /// Get a reference to an entity by its UUID, if it's being loaded by this + /// storage. + #[inline] + pub fn limited_get_by_uuid(&self, uuid: &Uuid) -> Option<&Arc> { + self.shared + .read() + .id_by_uuid .get(uuid) .and_then(|id| self.data_by_id.get(id)) } - /// Get a mutable reference to an entity by its uuid. + /// Get a mutable reference to an entity by its UUID, if it's being loaded + /// by this storage. #[inline] - pub fn get_mut_by_uuid(&mut self, uuid: &Uuid) -> Option<&mut EntityData> { - self.id_by_uuid + pub fn limited_get_mut_by_uuid(&mut self, uuid: &Uuid) -> Option<&mut Arc> { + self.shared + .read() + .id_by_uuid .get(uuid) .and_then(|id| self.data_by_id.get_mut(id)) } - /// Clear all entities in a chunk. + /// Get an entity in the shared storage by its UUID, if it exists. + #[inline] + pub fn get_by_uuid(&self, uuid: &Uuid) -> Option> { + self.shared.read().id_by_uuid.get(uuid).and_then(|id| { + self.shared + .read() + .data_by_id + .get(id) + .and_then(|e| e.upgrade()) + }) + } + + /// Clear all entities in a chunk. This will not clear them from the + /// shared storage, unless there are no other references to them. pub fn clear_chunk(&mut self, chunk: &ChunkPos) { - if let Some(entities) = self.id_by_chunk.remove(chunk) { - for entity_id in entities { - if let Some(entity) = self.data_by_id.remove(&entity_id) { - self.id_by_uuid.remove(&entity.uuid); - } else { - warn!("While clearing chunk {chunk:?}, found an entity that isn't in by_id {entity_id}."); + if let Some(entities) = self.shared.read().ids_by_chunk.get(chunk) { + for id in entities.iter() { + if let Some(entity) = self.data_by_id.remove(id) { + let uuid = entity.uuid; + drop(entity); + // maybe remove it from the storage + self.shared + .write() + .remove_entity_if_unused(*id, uuid, *chunk); } } + // for entity_id in entities { + // self.remove_by_id(entity_id); + // } } } - /// Updates an entity from its old chunk. + /// Move an entity from its old chunk to a new chunk. #[inline] pub fn update_entity_chunk( &mut self, @@ -104,36 +232,40 @@ impl EntityStorage { old_chunk: &ChunkPos, new_chunk: &ChunkPos, ) { - if let Some(entities) = self.id_by_chunk.get_mut(old_chunk) { + if let Some(entities) = self.shared.write().ids_by_chunk.get_mut(old_chunk) { entities.remove(&entity_id); } - self.id_by_chunk + self.shared + .write() + .ids_by_chunk .entry(*new_chunk) .or_default() .insert(entity_id); } - /// Get an iterator over all entities. - #[inline] - pub fn entities(&self) -> std::collections::hash_map::Values<'_, u32, EntityData> { - self.data_by_id.values() + pub fn find_one_entity(&self, mut f: F) -> Option> + where + F: FnMut(&Arc) -> bool, + { + for entity in self.shared.read().entities() { + if let Some(entity) = entity.upgrade() { + if f(&entity) { + return Some(entity); + } + } + } + None } - pub fn find_one_entity(&self, mut f: F) -> Option<&EntityData> + pub fn find_one_entity_in_chunk(&self, chunk: &ChunkPos, mut f: F) -> Option> where F: FnMut(&EntityData) -> bool, { - self.entities().find(|&entity| f(entity)) - } - - pub fn find_one_entity_in_chunk(&self, chunk: &ChunkPos, mut f: F) -> Option<&EntityData> - where - F: FnMut(&EntityData) -> bool, - { - if let Some(entities) = self.id_by_chunk.get(chunk) { + let shared = self.shared.read(); + if let Some(entities) = shared.ids_by_chunk.get(chunk) { for entity_id in entities { - if let Some(entity) = self.data_by_id.get(entity_id) { - if f(entity) { + if let Some(entity) = shared.data_by_id.get(entity_id).and_then(|e| e.upgrade()) { + if f(&entity) { return Some(entity); } } @@ -143,9 +275,81 @@ impl EntityStorage { } } -impl Default for EntityStorage { - fn default() -> Self { - Self::new() +impl WeakEntityStorage { + pub fn new() -> Self { + Self { + data_by_id: IntMap::default(), + ids_by_chunk: HashMap::default(), + id_by_uuid: HashMap::default(), + updates_received: IntMap::default(), + } + } + + /// Remove an entity from the storage if it has no strong references left. + /// Returns whether the entity was removed. + pub fn remove_entity_if_unused(&mut self, id: u32, uuid: Uuid, chunk: ChunkPos) -> bool { + if self.data_by_id.get(&id).and_then(|e| e.upgrade()).is_some() { + // if we could get the entity, that means there are still strong + // references to it + false + } else { + if self.ids_by_chunk.remove(&chunk).is_none() { + warn!("Tried to remove entity with id {id} from chunk {chunk:?} but it was not found."); + } + if self.id_by_uuid.remove(&uuid).is_none() { + warn!( + "Tried to remove entity with id {id} from uuid {uuid:?} but it was not found." + ); + } + if self.updates_received.remove(&id).is_none() { + // if this happens it means we weren't tracking the updates_received for the client (bad) + warn!( + "Tried to remove entity with id {id} from updates_received but it was not found." + ); + } + true + } + } + + /// Remove a chunk from the storage if the entities in it have no strong + /// references left. + pub fn remove_chunk_if_unused(&mut self, chunk: &ChunkPos) { + if let Some(entities) = self.ids_by_chunk.get(chunk) { + if entities.is_empty() { + self.ids_by_chunk.remove(chunk); + } + } + } + + /// Get an iterator over all entities in the shared storage. The iterator + /// is over `Weak`s, so you'll have to manually try to upgrade. + /// + /// # Examples + /// + /// ```rust + /// let mut storage = EntityStorage::new(); + /// storage.insert( + /// 0, + /// Arc::new(EntityData::new( + /// uuid, + /// Vec3::default(), + /// EntityMetadata::Player(metadata::Player::default()), + /// )), + /// ); + /// for entity in storage.shared.read().entities() { + /// if let Some(entity) = entity.upgrade() { + /// println!("Entity: {:?}", entity); + /// } + /// } + /// ``` + pub fn entities(&self) -> std::collections::hash_map::Values<'_, u32, Weak> { + self.data_by_id.values() + } + + /// Whether the entity with the given id is in the shared storage. + #[inline] + pub fn contains_id(&self, id: &u32) -> bool { + self.data_by_id.contains_key(id) } } @@ -158,7 +362,7 @@ mod tests { #[test] fn test_store_entity() { - let mut storage = EntityStorage::new(); + let mut storage = PartialEntityStorage::default(); assert!(storage.get_by_id(0).is_none()); let uuid = Uuid::from_u128(100); diff --git a/azalea-world/src/lib.rs b/azalea-world/src/lib.rs old mode 100755 new mode 100644 index 26cae205..05cc7d85 --- a/azalea-world/src/lib.rs +++ b/azalea-world/src/lib.rs @@ -1,174 +1,26 @@ #![feature(int_roundings)] +#![feature(error_generic_member_access)] +#![feature(provide_any)] mod bit_storage; mod chunk_storage; +mod container; pub mod entity; mod entity_storage; mod palette; +mod world; + +use std::backtrace::Backtrace; -use azalea_block::BlockState; -use azalea_buf::BufReadError; -use azalea_core::{BlockPos, ChunkPos, PositionDelta8, Vec3}; pub use bit_storage::BitStorage; -pub use chunk_storage::{Chunk, ChunkStorage}; -use entity::{Entity, EntityData}; -pub use entity_storage::EntityStorage; -use parking_lot::Mutex; -use std::{ - io::Cursor, - ops::{Index, IndexMut}, - sync::Arc, -}; +pub use chunk_storage::{Chunk, ChunkStorage, PartialChunkStorage, WeakChunkStorage}; +pub use container::*; +pub use entity_storage::{PartialEntityStorage, WeakEntityStorage}; use thiserror::Error; -use uuid::Uuid; - -/// A world is a collection of chunks and entities. They're called "levels" in Minecraft's source code. -#[derive(Debug, Default)] -pub struct World { - pub chunk_storage: ChunkStorage, - pub entity_storage: EntityStorage, -} +pub use world::*; #[derive(Error, Debug)] pub enum MoveEntityError { #[error("Entity doesn't exist")] - EntityDoesNotExist, -} - -impl World { - pub fn new(chunk_radius: u32, height: u32, min_y: i32) -> Self { - World { - chunk_storage: ChunkStorage::new(chunk_radius, height, min_y), - entity_storage: EntityStorage::new(), - } - } - - pub fn replace_with_packet_data( - &mut self, - pos: &ChunkPos, - data: &mut Cursor<&[u8]>, - ) -> Result<(), BufReadError> { - self.chunk_storage.replace_with_packet_data(pos, data) - } - - pub fn set_chunk(&mut self, pos: &ChunkPos, chunk: Option) -> Result<(), BufReadError> { - self[pos] = chunk.map(|c| Arc::new(Mutex::new(c))); - Ok(()) - } - - pub fn update_view_center(&mut self, pos: &ChunkPos) { - self.chunk_storage.view_center = *pos; - } - - pub fn get_block_state(&self, pos: &BlockPos) -> Option { - self.chunk_storage.get_block_state(pos) - } - - pub fn set_block_state(&mut self, pos: &BlockPos, state: BlockState) -> Option { - self.chunk_storage.set_block_state(pos, state) - } - - pub fn set_entity_pos(&mut self, entity_id: u32, new_pos: Vec3) -> Result<(), MoveEntityError> { - let mut entity = self - .entity_mut(entity_id) - .ok_or(MoveEntityError::EntityDoesNotExist)?; - - let old_chunk = ChunkPos::from(entity.pos()); - let new_chunk = ChunkPos::from(&new_pos); - // this is fine because we update the chunk below - unsafe { entity.move_unchecked(new_pos) }; - if old_chunk != new_chunk { - self.entity_storage - .update_entity_chunk(entity_id, &old_chunk, &new_chunk); - } - Ok(()) - } - - pub fn move_entity_with_delta( - &mut self, - entity_id: u32, - delta: &PositionDelta8, - ) -> Result<(), MoveEntityError> { - let mut entity = self - .entity_mut(entity_id) - .ok_or(MoveEntityError::EntityDoesNotExist)?; - let new_pos = entity.pos().with_delta(delta); - - let old_chunk = ChunkPos::from(entity.pos()); - let new_chunk = ChunkPos::from(&new_pos); - // this is fine because we update the chunk below - - unsafe { entity.move_unchecked(new_pos) }; - if old_chunk != new_chunk { - self.entity_storage - .update_entity_chunk(entity_id, &old_chunk, &new_chunk); - } - Ok(()) - } - - pub fn add_entity(&mut self, id: u32, entity: EntityData) { - self.entity_storage.insert(id, entity); - } - - pub fn height(&self) -> u32 { - self.chunk_storage.height - } - - pub fn min_y(&self) -> i32 { - self.chunk_storage.min_y - } - - pub fn entity_data_by_id(&self, id: u32) -> Option<&EntityData> { - self.entity_storage.get_by_id(id) - } - - pub fn entity_data_mut_by_id(&mut self, id: u32) -> Option<&mut EntityData> { - self.entity_storage.get_mut_by_id(id) - } - - pub fn entity(&self, id: u32) -> Option> { - let entity_data = self.entity_storage.get_by_id(id)?; - let entity_ptr = unsafe { entity_data.as_const_ptr() }; - Some(Entity::new(self, id, entity_ptr)) - } - - pub fn entity_mut(&mut self, id: u32) -> Option> { - let entity_data = self.entity_storage.get_mut_by_id(id)?; - let entity_ptr = unsafe { entity_data.as_ptr() }; - Some(Entity::new(self, id, entity_ptr)) - } - - pub fn entity_by_uuid(&self, uuid: &Uuid) -> Option<&EntityData> { - self.entity_storage.get_by_uuid(uuid) - } - - pub fn entity_mut_by_uuid(&mut self, uuid: &Uuid) -> Option<&mut EntityData> { - self.entity_storage.get_mut_by_uuid(uuid) - } - - /// Get an iterator over all entities. - #[inline] - pub fn entities(&self) -> std::collections::hash_map::Values<'_, u32, EntityData> { - self.entity_storage.entities() - } - - pub fn find_one_entity(&self, mut f: F) -> Option<&EntityData> - where - F: FnMut(&EntityData) -> bool, - { - self.entity_storage.find_one_entity(|entity| f(entity)) - } -} - -impl Index<&ChunkPos> for World { - type Output = Option>>; - - fn index(&self, pos: &ChunkPos) -> &Self::Output { - &self.chunk_storage[pos] - } -} -impl IndexMut<&ChunkPos> for World { - fn index_mut<'a>(&'a mut self, pos: &ChunkPos) -> &'a mut Self::Output { - &mut self.chunk_storage[pos] - } + EntityDoesNotExist(Backtrace), } diff --git a/azalea-world/src/world.rs b/azalea-world/src/world.rs new file mode 100644 index 00000000..257d9eb6 --- /dev/null +++ b/azalea-world/src/world.rs @@ -0,0 +1,181 @@ +use crate::{ + entity::{Entity, EntityData}, + Chunk, MoveEntityError, PartialChunkStorage, PartialEntityStorage, WeakChunkStorage, + WeakEntityStorage, +}; +use azalea_block::BlockState; +use azalea_buf::BufReadError; +use azalea_core::{BlockPos, ChunkPos, PositionDelta8, Vec3}; +use parking_lot::{Mutex, RwLock}; +use std::{backtrace::Backtrace, fmt::Debug}; +use std::{fmt::Formatter, io::Cursor, sync::Arc}; +use uuid::Uuid; + +/// A world is a collection of chunks and entities. They're called "levels" in Minecraft's source code. +#[derive(Default)] +pub struct World { + // we just need to keep a strong reference to `shared` so it doesn't get + // dropped, we don't need to do anything with it + _shared: Arc, + + pub chunk_storage: PartialChunkStorage, + pub entity_storage: PartialEntityStorage, +} + +/// A world where the chunks are stored as weak pointers. This is used for shared worlds. +#[derive(Default)] +pub struct WeakWorld { + pub chunk_storage: Arc>, + pub entity_storage: Arc>, +} + +impl World { + pub fn new(chunk_radius: u32, shared: Arc, owner_entity_id: u32) -> Self { + World { + _shared: shared.clone(), + chunk_storage: PartialChunkStorage::new(chunk_radius, shared.chunk_storage.clone()), + entity_storage: PartialEntityStorage::new( + shared.entity_storage.clone(), + owner_entity_id, + ), + } + } + + pub fn replace_with_packet_data( + &mut self, + pos: &ChunkPos, + data: &mut Cursor<&[u8]>, + ) -> Result<(), BufReadError> { + self.chunk_storage.replace_with_packet_data(pos, data) + } + + pub fn get_chunk(&self, pos: &ChunkPos) -> Option>> { + self.chunk_storage.get(pos) + } + + pub fn set_chunk(&mut self, pos: &ChunkPos, chunk: Option) -> Result<(), BufReadError> { + self.chunk_storage + .set(pos, chunk.map(|c| Arc::new(Mutex::new(c)))); + Ok(()) + } + + pub fn update_view_center(&mut self, pos: &ChunkPos) { + self.chunk_storage.view_center = *pos; + } + + pub fn get_block_state(&self, pos: &BlockPos) -> Option { + self.chunk_storage.get_block_state(pos) + } + + pub fn set_block_state(&mut self, pos: &BlockPos, state: BlockState) -> Option { + self.chunk_storage.set_block_state(pos, state) + } + + pub fn set_entity_pos(&mut self, entity_id: u32, new_pos: Vec3) -> Result<(), MoveEntityError> { + let mut entity = self + .entity_mut(entity_id) + .ok_or_else(|| MoveEntityError::EntityDoesNotExist(Backtrace::capture()))?; + let old_chunk = ChunkPos::from(entity.pos()); + let new_chunk = ChunkPos::from(&new_pos); + // this is fine because we update the chunk below + unsafe { entity.move_unchecked(new_pos) }; + if old_chunk != new_chunk { + self.entity_storage + .update_entity_chunk(entity_id, &old_chunk, &new_chunk); + } + Ok(()) + } + + pub fn move_entity_with_delta( + &mut self, + entity_id: u32, + delta: &PositionDelta8, + ) -> Result<(), MoveEntityError> { + let mut entity = self + .entity_mut(entity_id) + .ok_or_else(|| MoveEntityError::EntityDoesNotExist(Backtrace::capture()))?; + let new_pos = entity.pos().with_delta(delta); + + let old_chunk = ChunkPos::from(entity.pos()); + let new_chunk = ChunkPos::from(&new_pos); + // this is fine because we update the chunk below + + unsafe { entity.move_unchecked(new_pos) }; + if old_chunk != new_chunk { + self.entity_storage + .update_entity_chunk(entity_id, &old_chunk, &new_chunk); + } + Ok(()) + } + + pub fn add_entity(&mut self, id: u32, entity: EntityData) { + self.entity_storage.insert(id, entity); + } + + pub fn height(&self) -> u32 { + self.chunk_storage.height() + } + + pub fn min_y(&self) -> i32 { + self.chunk_storage.min_y() + } + + pub fn entity_data_by_id(&self, id: u32) -> Option> { + self.entity_storage.get_by_id(id) + } + + pub fn entity(&self, id: u32) -> Option> { + let entity_data = self.entity_storage.get_by_id(id)?; + let entity_ptr = unsafe { entity_data.as_ptr() }; + Some(Entity::new(self, id, entity_ptr)) + } + + /// Returns a mutable reference to the entity with the given ID. + pub fn entity_mut(&mut self, id: u32) -> Option> { + // no entity for you (we're processing this entity somewhere else) + if id != self.entity_storage.owner_entity_id && !self.entity_storage.maybe_update(id) { + return None; + } + + let entity_data = self.entity_storage.get_by_id(id)?; + let entity_ptr = unsafe { entity_data.as_ptr() }; + Some(Entity::new(self, id, entity_ptr)) + } + + pub fn entity_by_uuid(&self, uuid: &Uuid) -> Option> { + self.entity_storage.get_by_uuid(uuid) + } + + pub fn find_one_entity(&self, mut f: F) -> Option> + where + F: FnMut(&EntityData) -> bool, + { + self.entity_storage.find_one_entity(|entity| f(entity)) + } +} + +impl WeakWorld { + pub fn new(height: u32, min_y: i32) -> Self { + WeakWorld { + chunk_storage: Arc::new(RwLock::new(WeakChunkStorage::new(height, min_y))), + entity_storage: Arc::new(RwLock::new(WeakEntityStorage::new())), + } + } + + pub fn height(&self) -> u32 { + self.chunk_storage.read().height + } + + pub fn min_y(&self) -> i32 { + self.chunk_storage.read().min_y + } +} + +impl Debug for World { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("World") + .field("chunk_storage", &self.chunk_storage) + .field("entity_storage", &self.entity_storage) + .finish() + } +} diff --git a/azalea/Cargo.toml b/azalea/Cargo.toml old mode 100755 new mode 100644 index 92a689e0..498306dc --- a/azalea/Cargo.toml +++ b/azalea/Cargo.toml @@ -10,19 +10,23 @@ version = "0.4.0" [dependencies] anyhow = "^1.0.65" -async-trait = "^0.1.57" -azalea-block = { version = "0.4.0", path = "../azalea-block" } +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" } +azalea-physics = {version = "0.4.0", path = "../azalea-physics"} azalea-protocol = {version = "0.4.0", path = "../azalea-protocol"} -azalea-world = { version = "0.4.0", path = "../azalea-world" } +azalea-world = {version = "0.4.0", path = "../azalea-world"} +futures = "0.3.25" log = "0.4.17" +nohash-hasher = "0.2.0" num-traits = "0.2.15" parking_lot = {version = "^0.12.1", features = ["deadlock_detection"]} priority-queue = "1.3.0" thiserror = "^1.0.37" tokio = "^1.21.2" +uuid = "1.2.2" [dev-dependencies] anyhow = "^1.0.65" diff --git a/azalea/README.md b/azalea/README.md index d9aa1574..afd2feb4 100755 --- a/azalea/README.md +++ b/azalea/README.md @@ -1,3 +1,4 @@ Azalea is a framework for creating Minecraft bots. Internally, it's just a wrapper over azalea-client, adding useful functions for making bots. + diff --git a/azalea/examples/mine_a_chunk.rs b/azalea/examples/mine_a_chunk.rs old mode 100755 new mode 100644 index 2e30b2c5..f9b208a2 --- a/azalea/examples/mine_a_chunk.rs +++ b/azalea/examples/mine_a_chunk.rs @@ -1,13 +1,16 @@ -use azalea::{Account, Accounts, Client, Event, Swarm}; +use azalea::{prelude::*, SwarmEvent}; +use azalea::{Account, Client, Event, Swarm}; use parking_lot::Mutex; use std::sync::Arc; #[tokio::main] async fn main() { - let accounts = Accounts::new(); + let mut accounts = Vec::new(); + let mut states = Vec::new(); for i in 0..10 { - accounts.add(Account::offline(&format!("bot{}", i))); + accounts.push(Account::offline(&format!("bot{}", i))); + states.push(Arc::new(Mutex::new(State::default()))); } azalea::start_swarm(azalea::SwarmOptions { @@ -15,13 +18,15 @@ async fn main() { address: "localhost", swarm_state: State::default(), - state: State::default(), + states, - swarm_plugins: plugins![azalea_pathfinder::Plugin::default()], + swarm_plugins: plugins![], plugins: plugins![], - handle: Box::new(handle), - swarm_handle: Box::new(swarm_handle), + handle, + swarm_handle, + + join_delay: None, }) .await .unwrap(); @@ -37,9 +42,13 @@ async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> { Ok(()) } -async fn swarm_handle(swarm: Swarm, event: Event, state: SwarmState) -> anyhow::Result<()> { - match event { - Event::Login => { +async fn swarm_handle( + swarm: Swarm, + event: SwarmEvent, + state: SwarmState, +) -> anyhow::Result<()> { + match &event { + SwarmEvent::Login => { swarm.goto(azalea::BlockPos::new(0, 70, 0)).await; // or bots.goto_goal(pathfinder::Goals::Goto(azalea::BlockPos(0, 70, 0))).await; diff --git a/azalea/examples/potatobot/autoeat.rs b/azalea/examples/potatobot/autoeat.rs index 0f0ccc6d..89934fa2 100755 --- a/azalea/examples/potatobot/autoeat.rs +++ b/azalea/examples/potatobot/autoeat.rs @@ -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, event: Event, bot: Client) { match event { Event::UpdateHunger => { diff --git a/azalea/examples/potatobot/main.rs b/azalea/examples/potatobot/main.rs index e585c41d..8d40c48e 100755 --- a/azalea/examples/potatobot/main.rs +++ b/azalea/examples/potatobot/main.rs @@ -15,7 +15,7 @@ async fn main() { account, address: "localhost", state: State::default(), - plugins: plugins![autoeat::Plugin::default(), pathfinder::Plugin::default(),], + plugins: plugins![autoeat::Plugin, pathfinder::Plugin], handle, }) .await diff --git a/azalea/examples/pvp.rs b/azalea/examples/pvp.rs index 87d83c6d..157ad9e2 100755 --- a/azalea/examples/pvp.rs +++ b/azalea/examples/pvp.rs @@ -15,7 +15,7 @@ async fn main() { swarm_state: State::default(), state: State::default(), - swarm_plugins: plugins![pathfinder::Plugin::default()], + swarm_plugins: plugins![pathfinder::Plugin], plugins: plugins![], handle: Box::new(handle), @@ -32,7 +32,7 @@ struct State {} struct SwarmState {} async fn handle(bot: Client, event: Event, state: State) {} -async fn swarm_handle(swarm: Swarm, event: Event, state: State) { +async fn swarm_handle(swarm: Swarm, event: Event, state: State) { match event { Event::Tick => { // choose an arbitrary player within render distance to target diff --git a/azalea/src/bot.rs b/azalea/src/bot.rs old mode 100755 new mode 100644 index 0becaa62..0674c692 --- a/azalea/src/bot.rs +++ b/azalea/src/bot.rs @@ -4,9 +4,14 @@ 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 { + type State = State; + + fn build(&self) -> State { + State::default() + } } #[derive(Default, Clone)] @@ -14,6 +19,18 @@ pub struct State { jumping_once: Arc>, } +#[async_trait] +impl crate::PluginState for State { + async fn handle(self: Box, 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 +40,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::().unwrap().state.clone(); + let state = self.plugins.get::().unwrap().clone(); *state.jumping_once.lock() = true; } @@ -34,18 +51,6 @@ impl BotTrait for azalea_client::Client { } } -#[async_trait] -impl crate::Plugin for Plugin { - async fn handle(self: Box, 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; diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs old mode 100755 new mode 100644 index 89754409..7c9c660b --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -75,152 +75,19 @@ //! //! [`azalea_client`]: https://crates.io/crates/azalea-client +#![feature(trait_upcasting)] +#![feature(async_closure)] +#![allow(incomplete_features)] + mod bot; pub mod pathfinder; pub mod prelude; +mod start; +mod swarm; pub use azalea_client::*; pub use azalea_core::{BlockPos, Vec3}; -use azalea_protocol::ServerAddress; -use std::{future::Future, sync::Arc}; -use thiserror::Error; +pub use start::{start, Options}; +pub use swarm::*; pub type HandleFn = fn(Client, Event, S) -> Fut; - -/// The options that are passed to [`azalea::start`]. -/// -/// [`azalea::start`]: fn.start.html -pub struct Options -where - A: TryInto, - Fut: Future>, -{ - /// The address of the server that we're connecting to. This can be a - /// `&str`, [`ServerAddress`], or anything that implements - /// `TryInto`. - /// - /// [`ServerAddress`]: azalea_protocol::ServerAddress - pub address: A, - /// The account that's going to join the server. - pub account: Account, - /// The plugins that are going to be used. Plugins are external crates that - /// add extra functionality to Azalea. You should use the [`plugins`] macro - /// for this field. - /// - /// ```rust,no_run - /// plugins![azalea_pathfinder::Plugin::default()] - /// ``` - pub plugins: Plugins, - /// A struct that contains the data that you want your bot to remember - /// across events. - /// - /// # Examples - /// - /// ```rust - /// use parking_lot::Mutex; - /// use std::sync::Arc; - /// - /// #[derive(Default, Clone)] - /// struct State { - /// farming: Arc>, - /// } - /// ``` - pub state: S, - /// The function that's called whenever we get an event. - /// - /// # Examples - /// - /// ```rust - /// use azalea::prelude::*; - /// - /// async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> { - /// Ok(()) - /// } - /// ``` - pub handle: HandleFn, -} - -#[derive(Error, Debug)] -pub enum Error { - #[error("Invalid address")] - InvalidAddress, - #[error("Join error: {0}")] - Join(#[from] azalea_client::JoinError), -} - -/// Join a server and start handling events. This function will run forever until -/// it gets disconnected from the server. -/// -/// # Examples -/// -/// ```rust,no_run -/// let error = azalea::start(azalea::Options { -/// account, -/// address: "localhost", -/// state: State::default(), -/// plugins: plugins![azalea_pathfinder::Plugin::default()], -/// handle, -/// }).await; -/// ``` -pub async fn start< - S: Send + Sync + Clone + 'static, - A: Send + TryInto, - Fut: Future> + Send + 'static, ->( - options: Options, -) -> Result<(), Error> { - let address = match options.address.try_into() { - Ok(address) => address, - Err(_) => return Err(Error::InvalidAddress), - }; - - let (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); - - let state = options.state; - - while let Some(event) = rx.recv().await { - let cloned_plugins = (*bot.plugins).clone(); - for plugin in cloned_plugins.into_iter() { - tokio::spawn(plugin.handle(event.clone(), bot.clone())); - } - - tokio::spawn(bot::Plugin::handle( - Box::new(bot.plugins.get::().unwrap().clone()), - event.clone(), - bot.clone(), - )); - tokio::spawn(pathfinder::Plugin::handle( - Box::new(bot.plugins.get::().unwrap().clone()), - event.clone(), - bot.clone(), - )); - - tokio::spawn((options.handle)(bot.clone(), event.clone(), state.clone())); - } - - Ok(()) -} - -/// 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 - } - }; -} diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index f119c645..8a9d7540 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -13,9 +13,14 @@ 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 { + type State = State; + + fn build(&self) -> State { + State::default() + } } #[derive(Default, Clone)] @@ -25,10 +30,10 @@ pub struct State { } #[async_trait] -impl crate::Plugin for Plugin { +impl crate::PluginState for State { async fn handle(self: Box, 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 +107,8 @@ impl Trait for azalea_client::Client { let state = self .plugins - .get::() + .get::() .expect("Pathfinder plugin not installed!") - .state .clone(); // convert the Option> to a VecDeque *state.path.lock() = p.expect("no path").into_iter().collect(); @@ -127,7 +131,7 @@ fn tick_execute_path(bot: &mut Client, path: &mut VecDeque) { } if target.is_reached(&bot.entity()) { - println!("ok target {target:?} reached"); + // println!("ok target {target:?} reached"); path.pop_front(); if path.is_empty() { bot.walk(WalkDirection::None); @@ -165,13 +169,13 @@ impl Node { /// Returns whether the entity is at the node and should start going to the /// next node. pub fn is_reached(&self, entity: &EntityData) -> bool { - println!( - "entity.delta.y: {} {:?}=={:?}, self.vertical_vel={:?}", - entity.delta.y, - BlockPos::from(entity.pos()), - self.pos, - self.vertical_vel - ); + // println!( + // "entity.delta.y: {} {:?}=={:?}, self.vertical_vel={:?}", + // entity.delta.y, + // BlockPos::from(entity.pos()), + // self.pos, + // self.vertical_vel + // ); BlockPos::from(entity.pos()) == self.pos && match self.vertical_vel { VerticalVel::NoneMidair => entity.delta.y > -0.1 && entity.delta.y < 0.1, diff --git a/azalea/src/prelude.rs b/azalea/src/prelude.rs old mode 100755 new mode 100644 index 9fa1ac1a..30205e59 --- a/azalea/src/prelude.rs +++ b/azalea/src/prelude.rs @@ -2,5 +2,5 @@ pub use crate::bot::BotTrait; pub use crate::pathfinder::Trait; -pub use crate::plugins; +pub use crate::{plugins, swarm_plugins, Plugin}; pub use azalea_client::{Account, Client, Event}; diff --git a/azalea/src/start.rs b/azalea/src/start.rs new file mode 100644 index 00000000..c7d79261 --- /dev/null +++ b/azalea/src/start.rs @@ -0,0 +1,136 @@ +use crate::{bot, pathfinder, HandleFn}; +use azalea_client::{Account, Client, Plugins}; +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]; +/// ``` +/// +/// [`Plugin`]: crate::Plugin +#[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() +pub struct Options +where + A: TryInto, + Fut: Future>, +{ + /// The address of the server that we're connecting to. This can be a + /// `&str`, [`ServerAddress`], or anything that implements + /// `TryInto`. + /// + /// [`ServerAddress`]: azalea_protocol::ServerAddress + pub address: A, + /// The account that's going to join the server. + pub account: Account, + /// The plugins that are going to be used. Plugins are external crates that + /// add extra functionality to Azalea. You should use the [`plugins`] macro + /// for this field. + /// + /// ```rust,no_run + /// plugins![azalea_pathfinder::Plugin] + /// ``` + pub plugins: Plugins, + /// A struct that contains the data that you want your bot to remember + /// across events. + /// + /// # Examples + /// + /// ```rust + /// use parking_lot::Mutex; + /// use std::sync::Arc; + /// + /// #[derive(Default, Clone)] + /// struct State { + /// farming: Arc>, + /// } + /// ``` + pub state: S, + /// The function that's called whenever we get an event. + /// + /// # Examples + /// + /// ```rust + /// use azalea::prelude::*; + /// + /// async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> { + /// Ok(()) + /// } + /// ``` + pub handle: HandleFn, +} + +#[derive(Error, Debug)] +pub enum StartError { + #[error("Invalid address")] + InvalidAddress, + #[error("Join error: {0}")] + Join(#[from] azalea_client::JoinError), +} + +/// Join a server and start handling events. This function will run forever until +/// it gets disconnected from the server. +/// +/// # Examples +/// +/// ```rust,no_run +/// let error = azalea::start(azalea::Options { +/// account, +/// address: "localhost", +/// state: State::default(), +/// plugins: plugins![azalea_pathfinder::Plugin], +/// handle, +/// }).await; +/// ``` +pub async fn start< + S: Send + Sync + Clone + 'static, + A: Send + TryInto, + Fut: Future> + Send + 'static, +>( + options: Options, +) -> Result<(), StartError> { + let address = match options.address.try_into() { + Ok(address) => address, + Err(_) => return Err(StartError::InvalidAddress), + }; + + let (mut bot, mut rx) = Client::join(&options.account, address).await?; + + let mut plugins = options.plugins; + // DEFAULT PLUGINS + plugins.add(bot::Plugin); + plugins.add(pathfinder::Plugin); + + bot.plugins = Arc::new(plugins.build()); + + let state = options.state; + + while let Some(event) = rx.recv().await { + let cloned_plugins = (*bot.plugins).clone(); + for plugin in cloned_plugins.into_iter() { + tokio::spawn(plugin.handle(event.clone(), bot.clone())); + } + + tokio::spawn((options.handle)(bot.clone(), event.clone(), state.clone())); + } + + Ok(()) +} diff --git a/azalea/src/swarm/chat.rs b/azalea/src/swarm/chat.rs new file mode 100644 index 00000000..a39632f5 --- /dev/null +++ b/azalea/src/swarm/chat.rs @@ -0,0 +1,147 @@ +//! 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, +} + +impl crate::Plugin for Plugin { + type State = State; + + fn build(&self) -> State { + 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>, + pub tx: UnboundedSender, + pub swarm_state: SwarmState, +} + +#[derive(Clone)] +pub struct SwarmState { + pub chat_queue: Arc>>, + pub chat_min_index: Arc>, + pub rx: Arc>>, +} + +#[async_trait] +impl crate::PluginState for State { + async fn handle(self: Box, 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 { + // 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; + + // 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 + 1; + found = true; + break; + } + } + + if !found { + // didn't find the message, so fire the swarm event and add to the queue + self.tx + .send(m.clone()) + .expect("failed to send chat message to swarm"); + chat_queue.push_back(m); + *farthest_chat_index = chat_queue.len() - 1 + *chat_min_index; + } + } + } +} + +impl SwarmState { + pub fn new(swarm: Swarm) -> (Self, UnboundedSender) + 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)); + + (swarm_state, tx) + } + async fn start(self, swarm: Swarm) + 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 { + 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 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::() + .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; + } + } + + let mut chat_queue = self.chat_queue.lock(); + // remove all messages from the queue that are before the min index + for _ in 0..(new_chat_min_index - chat_min_index) { + chat_queue.pop_front(); + } + + // update the min index + *self.chat_min_index.lock() = new_chat_min_index; + } + } +} diff --git a/azalea/src/swarm/mod.rs b/azalea/src/swarm/mod.rs new file mode 100644 index 00000000..c45014d2 --- /dev/null +++ b/azalea/src/swarm/mod.rs @@ -0,0 +1,447 @@ +/// Swarms are a way to conveniently control many bots. +mod chat; +mod plugins; + +pub use self::plugins::*; +use crate::{bot, HandleFn}; +use azalea_client::{Account, ChatPacket, Client, Event, JoinError, Plugins}; +use azalea_protocol::{ + connect::{Connection, ConnectionError}, + resolver::{self, ResolverError}, + ServerAddress, +}; +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 thiserror::Error; +use tokio::sync::mpsc::{self, UnboundedSender}; + +/// A helper macro that generates a [`SwarmPlugins`] struct from a list of objects +/// that implement [`SwarmPlugin`]. +/// +/// ```rust,no_run +/// swarm_plugins![azalea_pathfinder::Plugin]; +/// ``` +#[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. +/// +/// Swarms are created from the [`azalea::start_swarm`] function. +/// +/// The `S` type parameter is the type of the state for individual bots. +/// It's used to make the [`Swarm::add`] function work. +/// +/// [`azalea::start_swarm`]: fn.start_swarm.html +#[derive(Clone)] +pub struct Swarm { + bot_datas: Arc>>, + + resolved_address: SocketAddr, + address: ServerAddress, + pub worlds: Arc>, + /// Plugins that are set for new bots + plugins: Plugins, + + bots_tx: UnboundedSender<(Option, (Client, S))>, + swarm_tx: UnboundedSender, +} + +/// An event about something that doesn't have to do with a single bot. +#[derive(Clone, Debug)] +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 = fn(Swarm, SwarmEvent, SS) -> Fut; + +/// The options that are passed to [`azalea::start_swarm`]. +/// +/// [`azalea::start_swarm`]: crate::start_swarm() +pub struct SwarmOptions +where + A: TryInto, + Fut: Future>, + SwarmFut: Future>, +{ + /// The address of the server that we're connecting to. This can be a + /// `&str`, [`ServerAddress`], or anything that implements + /// `TryInto`. + /// + /// [`ServerAddress`]: azalea_protocol::ServerAddress + pub address: A, + /// The accounts that are going to join the server. + pub accounts: Vec, + /// The plugins that are going to be used for all the bots. + /// + /// You can usually leave this as `plugins![]`. + pub plugins: Plugins, + /// The plugins that are going to be used for the swarm. + /// + /// You can usually leave this as `swarm_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, + /// The state for the overall swarm. + pub swarm_state: SS, + /// The function that's called every time a bot receives an [`Event`]. + pub handle: HandleFn, + /// The function that's called every time the swarm receives a [`SwarmEvent`]. + pub swarm_handle: SwarmHandleFn, + + /// How long we should wait between each bot joining the server. Set to + /// None to have every bot connect at the same time. None is different than + /// a duration of 0, since if a duration is present the bots will wait for + /// the previous one to be ready. + pub join_delay: Option, +} + +#[derive(Error, Debug)] +pub enum SwarmStartError { + #[error("Invalid address")] + InvalidAddress, + #[error(transparent)] + ResolveAddress(#[from] ResolverError), + #[error("Join error: {0}")] + Join(#[from] azalea_client::JoinError), +} + +/// Make a bot [`Swarm`]. +/// +/// [`Swarm`]: struct.Swarm.html +/// +/// # Examples +/// ```rust,no_run +/// use azalea::{prelude::*, Swarm, SwarmEvent}; +/// use azalea::{Account, Client, Event}; +/// use std::time::Duration; +/// +/// #[derive(Default, Clone)] +/// struct State {} +/// +/// #[derive(Default, Clone)] +/// struct SwarmState {} +/// +/// #[tokio::main] +/// async fn main() -> anyhow::Result<()> { +/// let mut accounts = Vec::new(); +/// let mut states = Vec::new(); +/// +/// for i in 0..10 { +/// accounts.push(Account::offline(&format!("bot{}", i))); +/// states.push(State::default()); +/// } +/// +/// loop { +/// let e = azalea::start_swarm(azalea::SwarmOptions { +/// accounts: accounts.clone(), +/// address: "localhost", +/// +/// states: states.clone(), +/// swarm_state: SwarmState::default(), +/// +/// plugins: plugins![], +/// swarm_plugins: swarm_plugins![], +/// +/// handle, +/// swarm_handle, +/// +/// join_delay: Some(Duration::from_millis(1000)), +/// }) +/// .await; +/// println!("{e:?}"); +/// } +/// } +/// +/// async fn handle(bot: Client, event: Event, _state: State) -> anyhow::Result<()> { +/// match &event { +/// _ => {} +/// } +/// Ok(()) +/// } +/// +/// async fn swarm_handle( +/// mut swarm: Swarm, +/// event: SwarmEvent, +/// _state: SwarmState, +/// ) -> anyhow::Result<()> { +/// match &event { +/// SwarmEvent::Disconnect(account) => { +/// // automatically reconnect after 5 seconds +/// tokio::time::sleep(Duration::from_secs(5)).await; +/// swarm.add(account, State::default()).await?; +/// } +/// SwarmEvent::Chat(m) => { +/// println!("{}", m.message().to_ansi(None)); +/// } +/// _ => {} +/// } +/// Ok(()) +/// } +pub async fn start_swarm< + S: Send + Sync + Clone + 'static, + SS: Send + Sync + Clone + 'static, + A: Send + TryInto, + Fut: Future> + Send + 'static, + SwarmFut: Future> + Send + 'static, +>( + options: SwarmOptions, +) -> Result<(), SwarmStartError> { + assert_eq!( + options.accounts.len(), + options.states.len(), + "There must be exactly one state per bot." + ); + + // convert the TryInto into a ServerAddress + let address: ServerAddress = match options.address.try_into() { + Ok(address) => address, + Err(_) => return Err(SwarmStartError::InvalidAddress), + }; + + // resolve the address + let resolved_address = resolver::resolve_address(&address).await?; + + let world_container = Arc::new(RwLock::new(WeakWorldContainer::default())); + + let mut plugins = options.plugins; + let swarm_plugins = options.swarm_plugins; + + // 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, mut bots_rx) = mpsc::unbounded_channel(); + let (swarm_tx, mut swarm_rx) = mpsc::unbounded_channel(); + + let mut swarm = Swarm { + bot_datas: Arc::new(Mutex::new(Vec::new())), + + resolved_address, + address, + worlds: world_container, + plugins, + + bots_tx, + + 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) { + swarm_clone + .add_with_exponential_backoff(account, state.clone()) + .await; + tokio::time::sleep(join_delay).await; + } + } else { + let swarm_borrow = &swarm_clone; + join_all(options.accounts.iter().zip(options.states).map( + async move |(account, state)| -> Result<(), JoinError> { + swarm_borrow + .clone() + .add_with_exponential_backoff(account, state.clone()) + .await; + Ok(()) + }, + )) + .await; + } + }); + + let swarm_state = options.swarm_state; + let mut internal_state = InternalSwarmState::default(); + + // 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 { + for plugin in swarm_plugins_clone.clone().into_iter() { + tokio::spawn(plugin.handle(event.clone(), swarm_clone.clone())); + } + tokio::spawn((options.swarm_handle)( + swarm_clone.clone(), + event, + swarm_state.clone(), + )); + } + }); + + // bot events + while let Some((Some(event), (bot, state))) = bots_rx.recv().await { + // bot event handling + let cloned_plugins = (*bot.plugins).clone(); + for plugin in cloned_plugins.into_iter() { + tokio::spawn(plugin.handle(event.clone(), bot.clone())); + } + + // swarm event handling + // remove this #[allow] when more checks are added + #[allow(clippy::single_match)] + match &event { + Event::Login => { + internal_state.bots_joined += 1; + if internal_state.bots_joined == swarm.bot_datas.lock().len() { + swarm_tx.send(SwarmEvent::Login).unwrap(); + } + } + _ => {} + } + + tokio::spawn((options.handle)(bot, event, state)); + } + + join_task.abort(); + + Ok(()) +} + +impl Swarm +where + S: Send + Sync + Clone + 'static, +{ + /// Add a new account to the swarm. You can remove it later by calling [`Client::disconnect`]. + pub async fn add(&mut self, account: &Account, state: S) -> Result { + let conn = Connection::new(&self.resolved_address).await?; + let (conn, game_profile) = Client::handshake(conn, account, &self.address.clone()).await?; + + // tx is moved to the bot so it can send us events + // rx is used to receive events from the bot + let (tx, mut rx) = mpsc::channel(1); + let mut bot = Client::new(game_profile, conn, Some(self.worlds.clone())); + tx.send(Event::Init).await.expect("Failed to send event"); + bot.start_tasks(tx); + + bot.plugins = Arc::new(self.plugins.clone().build()); + + let cloned_bots_tx = self.bots_tx.clone(); + let cloned_bot = bot.clone(); + let cloned_state = state.clone(); + 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), + // they're handled above in start_swarm + if let Err(e) = + cloned_bots_tx.send((Some(event), (cloned_bot.clone(), cloned_state.clone()))) + { + error!("Error sending event to swarm: {e}"); + } + } + // the bot disconnected, so we remove it from the swarm + let mut bot_datas = bot_datas.lock(); + let index = bot_datas + .iter() + .position(|(b, _)| b.profile.uuid == cloned_bot.profile.uuid) + .expect("bot disconnected but not found in swarm"); + bot_datas.remove(index); + + swarm_tx + .send(SwarmEvent::Disconnect(owned_account)) + .unwrap(); + }); + + self.bot_datas.lock().push((bot.clone(), state.clone())); + + Ok(bot) + } + + /// Add a new account to the swarm, retrying if it couldn't join. This will + /// run forever until the bot joins or the task is aborted. + /// + /// Exponential backoff means if it fails joining it will initially wait 10 + /// seconds, then 20, then 40, up to 2 minutes. + pub async fn add_with_exponential_backoff(&mut self, account: &Account, state: S) -> Client { + let mut disconnects = 0; + loop { + match self.add(account, state.clone()).await { + Ok(bot) => return bot, + Err(e) => { + disconnects += 1; + let delay = (Duration::from_secs(5) * 2u32.pow(disconnects)) + .min(Duration::from_secs(120)); + let username = account.username.clone(); + error!("Error joining {username}: {e}. Waiting {delay:?} and trying again."); + tokio::time::sleep(delay).await; + } + } + } + } +} + +impl IntoIterator for Swarm +where + S: Send + Sync + Clone + 'static, +{ + type Item = (Client, S); + type IntoIter = std::vec::IntoIter; + + /// Iterate over the bots and their states in this swarm. + /// + /// ```rust,no_run + /// for (bot, state) in swarm { + /// // ... + /// } + /// ``` + fn into_iter(self) -> Self::IntoIter { + self.bot_datas.lock().clone().into_iter() + } +} + +#[derive(Default)] +struct InternalSwarmState { + /// The number of bots connected to the server + pub bots_joined: usize, +} + +impl From for SwarmStartError { + fn from(e: ConnectionError) -> Self { + SwarmStartError::from(JoinError::from(e)) + } +} diff --git a/azalea/src/swarm/plugins.rs b/azalea/src/swarm/plugins.rs new file mode 100644 index 00000000..0c7cf2ae --- /dev/null +++ b/azalea/src/swarm/plugins.rs @@ -0,0 +1,134 @@ +use crate::{Swarm, SwarmEvent}; +use async_trait::async_trait; +use nohash_hasher::NoHashHasher; +use std::{ + any::{Any, TypeId}, + collections::HashMap, + hash::BuildHasherDefault, +}; + +type U64Hasher = BuildHasherDefault>; + +// kind of based on https://docs.rs/http/latest/src/http/extensions.rs.html +/// A map of plugin ids to [`SwarmPlugin`] trait objects. The client stores +/// this so we can keep the state for our [`Swarm`] plugins. +/// +/// If you're using azalea, you should generate this from the `swarm_plugins!` macro. +#[derive(Clone, Default)] +pub struct SwarmPlugins { + map: Option>, U64Hasher>>, +} + +#[derive(Clone)] +pub struct SwarmPluginStates { + map: Option>, U64Hasher>>, +} + +impl SwarmPluginStates { + pub fn get>(&self) -> Option<&T> { + self.map + .as_ref() + .and_then(|map| map.get(&TypeId::of::())) + .and_then(|boxed| (boxed.as_ref() as &dyn Any).downcast_ref::()) + } +} + +impl SwarmPlugins +where + S: 'static, +{ + /// Create a new empty set of plugins. + pub fn new() -> Self { + Self { map: None } + } + + /// Add a new plugin to this set. + pub fn add>(&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::(), Box::new(plugin)); + } + + /// Build our plugin states from this set of plugins. Note that if you're + /// using `azalea` you'll probably never need to use this as it's called + /// for you. + pub fn build(self) -> SwarmPluginStates { + 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 IntoIterator for SwarmPluginStates { + type Item = Box>; + type IntoIter = std::vec::IntoIter; + + /// Iterate over the plugin states. + fn into_iter(self) -> Self::IntoIter { + self.map + .map(|map| map.into_values().collect::>()) + .unwrap_or_default() + .into_iter() + } +} + +/// A `SwarmPluginState` keeps the current state of a plugin for a client. All +/// the fields must be atomic. Unique `SwarmPluginState`s are built from +/// [`SwarmPlugin`]s. +#[async_trait] +pub trait SwarmPluginState: Send + Sync + SwarmPluginStateClone + Any + 'static { + async fn handle(self: Box, event: SwarmEvent, swarm: Swarm); +} + +/// Swarm plugins can keep their own personal state ([`SwarmPluginState`]), +/// listen to [`SwarmEvent`]s, and add new functions to [`Swarm`]. +pub trait SwarmPlugin: Send + Sync + SwarmPluginClone + Any + 'static { + fn build(&self) -> Box>; +} + +/// An internal trait that allows SwarmPluginState to be cloned. +#[doc(hidden)] +pub trait SwarmPluginStateClone { + fn clone_box(&self) -> Box>; +} +impl SwarmPluginStateClone for T +where + T: 'static + SwarmPluginState + Clone, +{ + fn clone_box(&self) -> Box> { + Box::new(self.clone()) + } +} +impl Clone for Box> { + fn clone(&self) -> Self { + self.clone_box() + } +} + +/// An internal trait that allows SwarmPlugin to be cloned. +#[doc(hidden)] +pub trait SwarmPluginClone { + fn clone_box(&self) -> Box>; +} +impl SwarmPluginClone for T +where + T: 'static + SwarmPlugin + Clone, +{ + fn clone_box(&self) -> Box> { + Box::new(self.clone()) + } +} +impl Clone for Box> { + fn clone(&self) -> Self { + self.clone_box() + } +} diff --git a/bot/Cargo.toml b/bot/Cargo.toml index 6663d1f7..47b2a08f 100644 --- a/bot/Cargo.toml +++ b/bot/Cargo.toml @@ -10,7 +10,9 @@ version = "0.2.0" [dependencies] anyhow = "1.0.65" azalea = {path = "../azalea"} +azalea-protocol = {path = "../azalea-protocol"} env_logger = "0.9.1" parking_lot = {version = "^0.12.1", features = ["deadlock_detection"]} +rand = "0.8.5" tokio = "1.19.2" uuid = "1.1.2" diff --git a/bot/src/main.rs b/bot/src/main.rs old mode 100755 new mode 100644 index 942868e2..e50da728 --- a/bot/src/main.rs +++ b/bot/src/main.rs @@ -1,16 +1,21 @@ use azalea::pathfinder::BlockPosGoal; -use azalea::{prelude::*, BlockPos}; +// use azalea::ClientInformation; +use azalea::{prelude::*, BlockPos, Swarm, SwarmEvent, WalkDirection}; use azalea::{Account, Client, Event}; +use azalea_protocol::packets::game::serverbound_client_command_packet::ServerboundClientCommandPacket; +use std::time::Duration; #[derive(Default, Clone)] struct State {} +#[derive(Default, Clone)] +struct SwarmState {} + #[tokio::main] async fn main() -> anyhow::Result<()> { env_logger::init(); { - // only for #[cfg] use parking_lot::deadlock; use std::thread; use std::time::Duration; @@ -32,52 +37,125 @@ 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..7 { + 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, + + join_delay: Some(Duration::from_millis(1000)), + // join_delay: None, }) .await; println!("{e:?}"); } } -async fn handle(bot: Client, event: Event, _state: State) -> anyhow::Result<()> { +async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<()> { match event { + Event::Init => { + // bot.set_client_information(ClientInformation { + // view_distance: 2, + // ..Default::default() + // }) + // .await?; + } 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() == " goto" { - let target_pos_vec3 = *(bot - .world - .read() - .entity_by_uuid(&uuid::uuid!("6536bfed869548fd83a1ecd24cf2a0fd")) - .unwrap() - .pos()); - 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?; + } + let entity = bot + .world + .read() + .entity_by_uuid(&uuid::uuid!("6536bfed-8695-48fd-83a1-ecd24cf2a0fd")); + if let Some(entity) = entity { + if m.content() == "goto" { + let target_pos_vec3 = entity.pos(); + let target_pos: BlockPos = target_pos_vec3.into(); + bot.goto(BlockPosGoal::from(target_pos)); + } else if m.content() == "look" { + let target_pos_vec3 = entity.pos(); + let target_pos: BlockPos = target_pos_vec3.into(); + println!("target_pos: {:?}", target_pos); + bot.look_at(&target_pos.center()); + } else if m.content() == "jump" { + bot.set_jumping(true); + } else if m.content() == "walk" { + bot.walk(WalkDirection::Forward); + } else if m.content() == "stop" { + bot.set_jumping(false); + bot.walk(WalkDirection::None); + } else if m.content() == "lag" { + std::thread::sleep(Duration::from_millis(1000)); + } } } - Event::Initialize => { - println!("initialized"); - } - Event::Tick => { - // bot.jump(); + Event::Death(_) => { + bot.write_packet(ServerboundClientCommandPacket { + action: azalea_protocol::packets::game::serverbound_client_command_packet::Action::PerformRespawn, + }.get()).await?; } _ => {} } Ok(()) } + +async fn swarm_handle( + mut swarm: Swarm, + event: SwarmEvent, + _state: SwarmState, +) -> anyhow::Result<()> { + match &event { + SwarmEvent::Disconnect(account) => { + println!("bot got kicked! {}", account.username); + 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)); + if m.message().to_string() == " world" { + for (name, world) in &swarm.worlds.read().worlds { + println!("world name: {}", name); + if let Some(w) = world.upgrade() { + for chunk_pos in w.chunk_storage.read().chunks.values() { + println!("chunk: {:?}", chunk_pos); + } + } else { + println!("nvm world is gone"); + } + } + } + if m.message().to_string() == " hi" { + for (bot, _) in swarm { + bot.chat("hello").await?; + } + } + } + _ => {} + } + Ok(()) +}