aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock14
-rwxr-xr-xazalea-auth/src/auth.rs3
-rwxr-xr-xazalea-chat/src/base_component.rs2
-rwxr-xr-xazalea-chat/src/component.rs5
-rwxr-xr-xazalea-chat/src/style.rs2
-rwxr-xr-xazalea-chat/src/text_component.rs2
-rwxr-xr-xazalea-chat/src/translatable_component.rs4
-rw-r--r--azalea-client/Cargo.toml16
-rwxr-xr-xazalea-client/src/chat.rs25
-rw-r--r--azalea-client/src/client.rs500
-rw-r--r--[-rwxr-xr-x]azalea-client/src/lib.rs6
-rw-r--r--[-rwxr-xr-x]azalea-client/src/movement.rs39
-rwxr-xr-xazalea-client/src/player.rs17
-rw-r--r--azalea-client/src/plugins.rs106
-rwxr-xr-xazalea-core/src/direction.rs8
-rwxr-xr-xazalea-core/src/lib.rs10
-rwxr-xr-xazalea-crypto/src/signing.rs4
-rw-r--r--[-rwxr-xr-x]azalea-physics/Cargo.toml0
-rw-r--r--[-rwxr-xr-x]azalea-physics/src/collision/mod.rs0
-rw-r--r--azalea-physics/src/collision/world_collisions.rs4
-rw-r--r--[-rwxr-xr-x]azalea-physics/src/lib.rs10
-rwxr-xr-xazalea-protocol/src/connect.rs73
-rw-r--r--[-rwxr-xr-x]azalea-protocol/src/lib.rs17
-rwxr-xr-xazalea-protocol/src/packets/game/clientbound_player_chat_packet.rs14
-rwxr-xr-xazalea-protocol/src/packets/game/clientbound_system_chat_packet.rs2
-rw-r--r--[-rwxr-xr-x]azalea-world/Cargo.toml0
-rwxr-xr-xazalea-world/src/chunk_storage.rs167
-rw-r--r--azalea-world/src/container.rs54
-rw-r--r--azalea-world/src/entity/attributes.rs2
-rw-r--r--azalea-world/src/entity/mod.rs13
-rwxr-xr-xazalea-world/src/entity_storage.rs322
-rw-r--r--[-rwxr-xr-x]azalea-world/src/lib.rs170
-rw-r--r--azalea-world/src/world.rs181
-rw-r--r--[-rwxr-xr-x]azalea/Cargo.toml12
-rwxr-xr-xazalea/README.md1
-rw-r--r--[-rwxr-xr-x]azalea/examples/mine_a_chunk.rs29
-rwxr-xr-xazalea/examples/potatobot/autoeat.rs2
-rwxr-xr-xazalea/examples/potatobot/main.rs2
-rwxr-xr-xazalea/examples/pvp.rs4
-rw-r--r--[-rwxr-xr-x]azalea/src/bot.rs37
-rw-r--r--[-rwxr-xr-x]azalea/src/lib.rs149
-rw-r--r--azalea/src/pathfinder/mod.rs34
-rw-r--r--[-rwxr-xr-x]azalea/src/prelude.rs2
-rw-r--r--azalea/src/start.rs136
-rw-r--r--azalea/src/swarm/chat.rs147
-rw-r--r--azalea/src/swarm/mod.rs447
-rw-r--r--azalea/src/swarm/plugins.rs134
-rw-r--r--bot/Cargo.toml2
-rw-r--r--[-rwxr-xr-x]bot/src/main.rs132
49 files changed, 2247 insertions, 815 deletions
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<serde_json::Value>,
@@ -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<ProfileResponse, GetProfileError> {
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<Component>,
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<ChatFormatting> for TextColor {
}
}
-#[derive(Clone, Debug, Default)]
+#[derive(Clone, Debug, Default, PartialEq)]
pub struct Style {
// these are options instead of just bools because None is different than false in this case
pub color: Option<TextColor>,
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<ClientboundPlayerChatPacket>),
@@ -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<tokio::sync::Mutex<ReadConnection<ClientboundGamePacket>>>,
pub write_conn: Arc<tokio::sync::Mutex<WriteConnection<ServerboundGamePacket>>>,
pub entity_id: Arc<RwLock<u32>>,
+ /// The world that this client has access to. This supports shared worlds.
pub world: Arc<RwLock<World>>,
+ /// 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<RwLock<WeakWorldContainer>>,
+ pub world_name: Arc<RwLock<Option<ResourceLocation>>>,
pub physics_state: Arc<Mutex<PhysicsState>>,
pub client_information: Arc<RwLock<ClientInformation>>,
pub dead: Arc<Mutex<bool>>,
/// Plugins are a way for other crates to add custom functionality to the
/// client and keep state. If you're not making a plugin and you're using
/// the `azalea` crate. you can ignore this field.
- pub plugins: Arc<Plugins>,
+ pub plugins: Arc<PluginStates>,
/// A map of player uuids to their information in the tab list
pub players: Arc<RwLock<HashMap<Uuid, PlayerInfo>>>,
tasks: Arc<Mutex<Vec<JoinHandle<()>>>>,
@@ -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<Event>),
}
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<ClientboundGamePacket, ServerboundGamePacket>,
+ world_container: Option<Arc<RwLock<WeakWorldContainer>>>,
+ ) -> 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<dyn std::error::Error> {
+ /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// 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<ServerAddress>,
- ) -> Result<(Self, UnboundedReceiver<Event>), JoinError> {
+ ) -> Result<(Self, Receiver<Event>), 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<ClientboundHandshakePacket, ServerboundHandshakePacket>,
+ account: &Account,
+ address: &ServerAddress,
+ ) -> Result<
+ (
+ Connection<ClientboundGamePacket, ServerboundGamePacket>,
+ 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<Event>) {
+ /// Start the protocol and game tick loop.
+ #[doc(hidden)]
+ pub fn start_tasks(&self, tx: Sender<Event>) {
+ // 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<Event>) {
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<Event>,
+ tx: &Sender<Event>,
) -> 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<Event>) {
+ async fn game_tick_loop(mut client: Client, tx: Sender<Event>) {
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<Event>) {
+ async fn game_tick(client: &mut Client, tx: &Sender<Event>) {
// 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<WeakWorld> {
+ 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<RwLockWriteGuard<World>> {
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<RwLockReadGuard<World>> {
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<dyn std::error::Error>> {
+ /// 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
index ebcc4477..91c8cd91 100755..100644
--- 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
index 87ac8d85..5fca924b 100755..100644
--- 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<MoveEntityError> 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<Component>,
}
-
-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,42 +10,65 @@ use std::{
type U64Hasher = BuildHasherDefault<NoHashHasher<u64>>;
// kind of based on https://docs.rs/http/latest/src/http/extensions.rs.html
-/// A map of plugin ids to Plugin trait objects. The client stores this so we
-/// can keep the state for our plugins.
+#[derive(Clone, Default)]
+pub struct PluginStates {
+ map: Option<HashMap<TypeId, Box<dyn PluginState>, U64Hasher>>,
+}
+
+/// A map of 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 azalea, you should generate this from the `plugins!` macro.
+/// If you're using the azalea crate, you should generate this from the
+/// `plugins!` macro.
#[derive(Clone, Default)]
pub struct Plugins {
- map: Option<HashMap<TypeId, Box<dyn Plugin>, U64Hasher>>,
+ map: Option<HashMap<TypeId, Box<dyn AnyPlugin>, U64Hasher>>,
+}
+
+impl PluginStates {
+ pub fn get<T: PluginState>(&self) -> Option<&T> {
+ self.map
+ .as_ref()
+ .and_then(|map| map.get(&TypeId::of::<T>()))
+ .and_then(|boxed| (boxed.as_ref() as &dyn Any).downcast_ref::<T>())
+ }
}
impl Plugins {
+ /// Create a new empty set of plugins.
pub fn new() -> Self {
Self::default()
}
- pub fn add<T: Plugin>(&mut self, plugin: T) {
+ /// Add a new plugin to this set.
+ pub fn add<T: Plugin + Clone>(&mut self, plugin: T) {
if self.map.is_none() {
self.map = Some(HashMap::with_hasher(BuildHasherDefault::default()));
}
self.map
.as_mut()
.unwrap()
- .insert(TypeId::of::<T>(), Box::new(plugin));
+ .insert(TypeId::of::<T::State>(), Box::new(plugin));
}
- pub fn get<T: Plugin>(&self) -> Option<&T> {
- self.map
- .as_ref()
- .and_then(|map| map.get(&TypeId::of::<T>()))
- .and_then(|boxed| (boxed.as_ref() as &dyn Any).downcast_ref::<T>())
+ /// 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 Plugins {
- type Item = Box<dyn Plugin>;
+impl IntoIterator for PluginStates {
+ type Item = Box<dyn PluginState>;
type IntoIter = std::vec::IntoIter<Self::Item>;
+ /// Iterate over the plugin states.
fn into_iter(self) -> Self::IntoIter {
self.map
.map(|map| map.into_values().collect::<Vec<_>>())
@@ -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<Self>, event: Event, bot: Client);
}
-/// An internal trait that allows Plugin to be cloned.
+/// 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;
+}
+
+/// AnyPlugin is basically a Plugin but without the State associated type
+/// it has to exist so we can do a hashmap with Box<dyn AnyPlugin>
+#[doc(hidden)]
+pub trait AnyPlugin: Send + Sync + Any + AnyPluginClone + 'static {
+ fn build(&self) -> Box<dyn PluginState>;
+}
+
+impl<S: PluginState, B: Plugin<State = S> + Clone> AnyPlugin for B {
+ fn build(&self) -> Box<dyn PluginState> {
+ Box::new(self.build())
+ }
+}
+
+/// An internal trait that allows PluginState to be cloned.
+#[doc(hidden)]
+pub trait PluginStateClone {
+ fn clone_box(&self) -> Box<dyn PluginState>;
+}
+impl<T> PluginStateClone for T
+where
+ T: 'static + PluginState + Clone,
+{
+ fn clone_box(&self) -> Box<dyn PluginState> {
+ Box::new(self.clone())
+ }
+}
+impl Clone for Box<dyn PluginState> {
+ fn clone(&self) -> Self {
+ self.clone_box()
+ }
+}
+
+/// An internal trait that allows AnyPlugin to be cloned.
#[doc(hidden)]
-pub trait PluginClone {
- fn clone_box(&self) -> Box<dyn Plugin>;
+pub trait AnyPluginClone {
+ fn clone_box(&self) -> Box<dyn AnyPlugin>;
}
-impl<T> PluginClone for T
+impl<T> AnyPluginClone for T
where
T: 'static + Plugin + Clone,
{
- fn clone_box(&self) -> Box<dyn Plugin> {
+ fn clone_box(&self) -> Box<dyn AnyPlugin> {
Box::new(self.clone())
}
}
-impl Clone for Box<dyn Plugin> {
+impl Clone for Box<dyn AnyPlugin> {
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<u8>,
}
-#[derive(Clone, Debug, Default, McBuf)]
+#[derive(Clone, Debug, Default, McBuf, PartialEq)]
pub struct MessageSignature {
pub bytes: Vec<u8>,
}
-#[derive(Clone, Debug, McBuf)]
+#[derive(Clone, Debug, McBuf, PartialEq)]
pub struct SignedMessageHeader {
pub previous_signature: Option<MessageSignature>,
pub sender: Uuid,
diff --git a/azalea-physics/Cargo.toml b/azalea-physics/Cargo.toml
index 7619db80..7619db80 100755..100644
--- a/azalea-physics/Cargo.toml
+++ b/azalea-physics/Cargo.toml
diff --git a/azalea-physics/src/collision/mod.rs b/azalea-physics/src/collision/mod.rs
index 7fb2cf97..7fb2cf97 100755..100644
--- a/azalea-physics/src/collision/mod.rs
+++ b/azalea-physics/src/collision/mod.rs
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<Mutex<Chunk>>> {
+ fn get_chunk(&self, block_x: i32, block_z: i32) -> Option<Arc<Mutex<Chunk>>> {
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
index 2295e6f2..34d31a0e 100755..100644
--- a/azalea-physics/src/lib.rs
+++ b/azalea-physics/src/lib.rs
@@ -231,7 +231,10 @@ fn jump_boost_power<D: DerefMut<Target = World>>(_entity: &Entity<D>) -> 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<W: ProtocolPacket> {
///
/// 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<dyn std::error::Error>> {
/// 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<W: ProtocolPacket> {
/// 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<ClientboundLoginPacket, ServerboundLoginPacket> {
/// # 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;
+ ///
+ /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
+ /// 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 connection = azalea::Connection::new(&server_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
index 0fae75b1..052e740f 100755..100644
--- 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<SocketAddr> 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<Component>,
}
-#[derive(Clone, Debug, McBuf)]
+#[derive(Clone, Debug, McBuf, PartialEq)]
pub struct PlayerChatMessage {
pub signed_header: SignedMessageHeader,
pub header_signature: MessageSignature,
@@ -41,7 +41,7 @@ pub struct PlayerChatMessage {
pub filter_mask: FilterMask,
}
-#[derive(Clone, Debug, McBuf)]
+#[derive(Clone, Debug, PartialEq, McBuf)]
pub struct SignedMessageBody {
pub content: ChatMessageContent,
pub timestamp: u64,
@@ -117,7 +117,7 @@ impl ChatType {
}
}
-#[derive(Clone, Debug, McBuf)]
+#[derive(Clone, Debug, McBuf, PartialEq)]
pub struct LastSeenMessagesEntry {
pub profile_id: Uuid,
pub last_signature: MessageSignature,
@@ -129,14 +129,14 @@ pub struct LastSeenMessagesUpdate {
pub last_received: Option<LastSeenMessagesEntry>,
}
-#[derive(Clone, Debug, McBuf)]
+#[derive(Clone, Debug, McBuf, PartialEq)]
pub struct ChatMessageContent {
pub plain: String,
/// Only sent if the decorated message is different than the plain.
pub decorated: Option<Component>,
}
-#[derive(Clone, Debug, McBuf)]
+#[derive(Clone, Debug, McBuf, PartialEq)]
pub enum FilterMask {
PassThrough,
FullyFiltered,
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
index ecff03d3..ecff03d3 100755..100644
--- a/azalea-world/Cargo.toml
+++ b/azalea-world/Cargo.toml
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<RwLock<WeakChunkStorage>>,
+
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<Option<Arc<Mutex<Chunk>>>>,
}
+/// 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<ChunkPos, Weak<Mutex<Chunk>>>,
+}
+
+/// A storage of potentially infinite chunks in a world. Chunks are stored as
+/// an `Arc<Mutex>` so they can be shared across threads.
+pub struct ChunkStorage {
+ pub height: u32,
+ pub min_y: i32,
+ pub chunks: HashMap<ChunkPos, Arc<Mutex<Chunk>>>,
+}
+
+/// 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<Section>,
}
+/// 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<RwLock<WeakChunkStorage>>) -> 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<BlockState> {
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<BlockState> {
- 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<Arc<Mutex<Chunk>>>;
+ /// 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<Mutex<Chunk>>> {
+ 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()
}
-}
-impl IndexMut<&ChunkPos> for ChunkStorage {
- fn index_mut<'a>(&'a mut self, pos: &ChunkPos) -> &'a mut Self::Output {
+ /// 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<Arc<Mutex<Chunk>>>> {
+ if !self.in_range(pos) {
+ return None;
+ }
+
let index = self.get_index(pos);
- &mut self.chunks[index]
+ Some(&mut self.chunks[index])
+ }
+
+ /// Get a chunk,
+ pub fn get(&self, pos: &ChunkPos) -> Option<Arc<Mutex<Chunk>>> {
+ 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<Arc<Mutex<Chunk>>>) {
+ 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 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, Arc::new(RwLock::new(WeakChunkStorage::default())))
+ }
+}
+impl Default for WeakChunkStorage {
fn default() -> Self {
- Self::new(8, 384, -64)
+ 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<ResourceLocation, Weak<WeakWorld>>,
+}
+
+impl WeakWorldContainer {
+ pub fn new() -> Self {
+ WeakWorldContainer {
+ worlds: HashMap::new(),
+ }
+ }
+
+ /// Get a world from the container.
+ pub fn get(&self, name: &ResourceLocation) -> Option<Arc<WeakWorld>> {
+ 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<WeakWorld> {
+ 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
+//! <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<EntityData> {
- 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<EntityData> {
+ /// The entity MUST exist for at least as long as this pointer exists.
+ pub unsafe fn as_ptr(&self) -> NonNull<EntityData> {
// 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<u32, EntityData>,
- id_by_chunk: HashMap<ChunkPos, IntSet<u32>>,
+// 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<RwLock<WeakEntityStorage>>,
+
+ /// The entity id of the player that owns this struct.
+ pub owner_entity_id: u32,
+ pub updates_received: IntMap<u32, u32>,
+ /// Strong references to the entities we have loaded.
+ data_by_id: IntMap<u32, Arc<EntityData>>,
+}
+
+/// 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<u32, Weak<EntityData>>,
+ /// An index of all the entity ids we know are in a chunk
+ ids_by_chunk: HashMap<ChunkPos, IntSet<u32>>,
+ /// An index of entity ids by their UUIDs
id_by_uuid: HashMap<Uuid, u32>,
+
+ pub updates_received: IntMap<u32, u32>,
}
-impl EntityStorage {
- pub fn new() -> Self {
+impl PartialEntityStorage {
+ pub fn new(shared: Arc<RwLock<WeakEntityStorage>>, 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<EntityData>> {
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<EntityData>> {
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<Arc<EntityData>> {
+ 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<EntityData>> {
+ 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<EntityData>> {
+ 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<Arc<EntityData>> {
+ 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<F>(&self, mut f: F) -> Option<&EntityData>
+ pub fn find_one_entity<F>(&self, mut f: F) -> Option<Arc<EntityData>>
where
- F: FnMut(&EntityData) -> bool,
+ F: FnMut(&Arc<EntityData>) -> bool,
{
- self.entities().find(|&entity| f(entity))
+ 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_in_chunk<F>(&self, chunk: &ChunkPos, mut f: F) -> Option<&EntityData>
+ pub fn find_one_entity_in_chunk<F>(&self, chunk: &ChunkPos, mut f: F) -> Option<Arc<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<EntityData>`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<EntityData>> {
+ 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
index 26cae205..05cc7d85 100755..100644
--- 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<Chunk>) -> 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<BlockState> {
- self.chunk_storage.get_block_state(pos)
- }
-
- pub fn set_block_state(&mut self, pos: &BlockPos, state: BlockState) -> Option<BlockState> {
- 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<Entity<&World>> {
- 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<Entity<'_, &mut World>> {
- 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<F>(&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<Arc<Mutex<Chunk>>>;
-
- 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<WeakWorld>,
+
+ 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<RwLock<WeakChunkStorage>>,
+ pub entity_storage: Arc<RwLock<WeakEntityStorage>>,
+}
+
+impl World {
+ pub fn new(chunk_radius: u32, shared: Arc<WeakWorld>, 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<Arc<Mutex<Chunk>>> {
+ self.chunk_storage.get(pos)
+ }
+
+ pub fn set_chunk(&mut self, pos: &ChunkPos, chunk: Option<Chunk>) -> 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<BlockState> {
+ self.chunk_storage.get_block_state(pos)
+ }
+
+ pub fn set_block_state(&mut self, pos: &BlockPos, state: BlockState) -> Option<BlockState> {
+ 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<Arc<EntityData>> {
+ self.entity_storage.get_by_id(id)
+ }
+
+ pub fn entity(&self, id: u32) -> Option<Entity<&World>> {
+ 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<Entity<'_, &mut World>> {
+ // 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<Arc<EntityData>> {
+ self.entity_storage.get_by_uuid(uuid)
+ }
+
+ pub fn find_one_entity<F>(&self, mut f: F) -> Option<Arc<EntityData>>
+ 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
index 92a689e0..498306dc 100755..100644
--- 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
index 2e30b2c5..f9b208a2 100755..100644
--- 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<State>,
+ 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<Self>, 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<S>(swarm: Swarm<S>, 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
index 0becaa62..0674c692 100755..100644
--- 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<Mutex<bool>>,
}
+#[async_trait]
+impl crate::PluginState for State {
+ async fn handle(self: Box<Self>, event: Event, mut bot: Client) {
+ if let Event::Tick = event {
+ if *self.jumping_once.lock() && bot.jumping() {
+ *self.jumping_once.lock() = false;
+ bot.set_jumping(false);
+ }
+ }
+ }
+}
+
pub trait BotTrait {
fn jump(&mut self);
fn look_at(&mut self, pos: &Vec3);
@@ -23,7 +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::<Plugin>().unwrap().state.clone();
+ let state = self.plugins.get::<State>().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<Self>, event: Event, mut bot: Client) {
- if let Event::Tick = event {
- if *self.state.jumping_once.lock() && bot.jumping() {
- *self.state.jumping_once.lock() = false;
- bot.set_jumping(false);
- }
- }
- }
-}
-
fn direction_looking_at(current: &Vec3, target: &Vec3) -> (f32, f32) {
// borrowed from mineflayer's Bot.lookAt because i didn't want to do math
let delta = target - current;
diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs
index 89754409..7c9c660b 100755..100644
--- 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<Fut, S> = fn(Client, Event, S) -> Fut;
-
-/// The options that are passed to [`azalea::start`].
-///
-/// [`azalea::start`]: fn.start.html
-pub struct Options<S, A, Fut>
-where
- A: TryInto<ServerAddress>,
- Fut: Future<Output = Result<(), anyhow::Error>>,
-{
- /// The address of the server that we're connecting to. This can be a
- /// `&str`, [`ServerAddress`], or anything that implements
- /// `TryInto<ServerAddress>`.
- ///
- /// [`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<Mutex<bool>>,
- /// }
- /// ```
- 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<Fut, S>,
-}
-
-#[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<ServerAddress>,
- Fut: Future<Output = Result<(), anyhow::Error>> + Send + 'static,
->(
- options: Options<S, A, Fut>,
-) -> Result<(), Error> {
- let address = match options.address.try_into() {
- Ok(address) => address,
- Err(_) => return Err(Error::InvalidAddress),
- };
-
- let (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::<bot::Plugin>().unwrap().clone()),
- event.clone(),
- bot.clone(),
- ));
- tokio::spawn(pathfinder::Plugin::handle(
- Box::new(bot.plugins.get::<pathfinder::Plugin>().unwrap().clone()),
- event.clone(),
- bot.clone(),
- ));
-
- tokio::spawn((options.handle)(bot.clone(), event.clone(), state.clone()));
- }
-
- 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<Self>, event: Event, mut bot: Client) {
if let Event::Tick = event {
- let mut path = self.state.path.lock();
+ let mut path = self.path.lock();
if !path.is_empty() {
tick_execute_path(&mut bot, &mut path);
@@ -102,9 +107,8 @@ impl Trait for azalea_client::Client {
let state = self
.plugins
- .get::<Plugin>()
+ .get::<State>()
.expect("Pathfinder plugin not installed!")
- .state
.clone();
// convert the Option<Vec<Node>> to a VecDeque<Node>
*state.path.lock() = p.expect("no path").into_iter().collect();
@@ -127,7 +131,7 @@ fn tick_execute_path(bot: &mut Client, path: &mut VecDeque<Node>) {
}
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
index 9fa1ac1a..30205e59 100755..100644
--- 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<S, A, Fut>
+where
+ A: TryInto<ServerAddress>,
+ Fut: Future<Output = Result<(), anyhow::Error>>,
+{
+ /// The address of the server that we're connecting to. This can be a
+ /// `&str`, [`ServerAddress`], or anything that implements
+ /// `TryInto<ServerAddress>`.
+ ///
+ /// [`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<Mutex<bool>>,
+ /// }
+ /// ```
+ 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<Fut, S>,
+}
+
+#[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<ServerAddress>,
+ Fut: Future<Output = Result<(), anyhow::Error>> + Send + 'static,
+>(
+ options: Options<S, A, Fut>,
+) -> 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<ChatPacket>,
+}
+
+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<Mutex<usize>>,
+ pub tx: UnboundedSender<ChatPacket>,
+ pub swarm_state: SwarmState,
+}
+
+#[derive(Clone)]
+pub struct SwarmState {
+ pub chat_queue: Arc<Mutex<VecDeque<ChatPacket>>>,
+ pub chat_min_index: Arc<Mutex<usize>>,
+ pub rx: Arc<tokio::sync::Mutex<UnboundedReceiver<ChatPacket>>>,
+}
+
+#[async_trait]
+impl crate::PluginState for State {
+ async fn handle(self: Box<Self>, event: Event, _bot: Client) {
+ // we're allowed to access Plugin::swarm_state since it's shared for every bot
+ if let Event::Chat(m) = event {
+ // 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<S>(swarm: Swarm<S>) -> (Self, UnboundedSender<ChatPacket>)
+ where
+ S: Send + Sync + Clone + 'static,
+ {
+ let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
+
+ let swarm_state = SwarmState {
+ chat_queue: Arc::new(Mutex::new(VecDeque::new())),
+ chat_min_index: Arc::new(Mutex::new(0)),
+ rx: Arc::new(tokio::sync::Mutex::new(rx)),
+ };
+ tokio::spawn(swarm_state.clone().start(swarm));
+
+ (swarm_state, tx)
+ }
+ async fn start<S>(self, swarm: Swarm<S>)
+ where
+ S: Send + Sync + Clone + 'static,
+ {
+ // it should never be locked unless we reused the same plugin for two swarms (bad)
+ let mut rx = self.rx.lock().await;
+ while let Some(m) = rx.recv().await {
+ 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::<State>()
+ .expect("Chat plugin not installed")
+ .farthest_chat_index
+ .lock();
+ if this_farthest_chat_index < new_chat_min_index {
+ new_chat_min_index = this_farthest_chat_index;
+ }
+ }
+
+ 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<S> {
+ bot_datas: Arc<Mutex<Vec<(Client, S)>>>,
+
+ resolved_address: SocketAddr,
+ address: ServerAddress,
+ pub worlds: Arc<RwLock<WeakWorldContainer>>,
+ /// Plugins that are set for new bots
+ plugins: Plugins,
+
+ bots_tx: UnboundedSender<(Option<Event>, (Client, S))>,
+ swarm_tx: UnboundedSender<SwarmEvent>,
+}
+
+/// 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<Fut, S, SS> = fn(Swarm<S>, SwarmEvent, SS) -> Fut;
+
+/// The options that are passed to [`azalea::start_swarm`].
+///
+/// [`azalea::start_swarm`]: crate::start_swarm()
+pub struct SwarmOptions<S, SS, A, Fut, SwarmFut>
+where
+ A: TryInto<ServerAddress>,
+ Fut: Future<Output = Result<(), anyhow::Error>>,
+ SwarmFut: Future<Output = Result<(), anyhow::Error>>,
+{
+ /// The address of the server that we're connecting to. This can be a
+ /// `&str`, [`ServerAddress`], or anything that implements
+ /// `TryInto<ServerAddress>`.
+ ///
+ /// [`ServerAddress`]: azalea_protocol::ServerAddress
+ pub address: A,
+ /// The accounts that are going to join the server.
+ pub accounts: Vec<Account>,
+ /// 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<S>,
+ /// The individual bot states. This must be the same length as `accounts`,
+ /// since each bot gets one state.
+ pub states: Vec<S>,
+ /// 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<Fut, S>,
+ /// The function that's called every time the swarm receives a [`SwarmEvent`].
+ pub swarm_handle: SwarmHandleFn<SwarmFut, S, SS>,
+
+ /// 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<std::time::Duration>,
+}
+
+#[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<State>,
+/// 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<ServerAddress>,
+ Fut: Future<Output = Result<(), anyhow::Error>> + Send + 'static,
+ SwarmFut: Future<Output = Result<(), anyhow::Error>> + Send + 'static,
+>(
+ options: SwarmOptions<S, SS, A, Fut, SwarmFut>,
+) -> Result<(), SwarmStartError> {
+ assert_eq!(
+ options.accounts.len(),
+ options.states.len(),
+ "There must be exactly one state per bot."
+ );
+
+ // convert the TryInto<ServerAddress> 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<S> Swarm<S>
+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<Client, JoinError> {
+ 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<S> IntoIterator for Swarm<S>
+where
+ S: Send + Sync + Clone + 'static,
+{
+ type Item = (Client, S);
+ type IntoIter = std::vec::IntoIter<Self::Item>;
+
+ /// 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<ConnectionError> 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<NoHashHasher<u64>>;
+
+// 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<S> {
+ map: Option<HashMap<TypeId, Box<dyn SwarmPlugin<S>>, U64Hasher>>,
+}
+
+#[derive(Clone)]
+pub struct SwarmPluginStates<S> {
+ map: Option<HashMap<TypeId, Box<dyn SwarmPluginState<S>>, U64Hasher>>,
+}
+
+impl<S> SwarmPluginStates<S> {
+ pub fn get<T: SwarmPluginState<S>>(&self) -> Option<&T> {
+ self.map
+ .as_ref()
+ .and_then(|map| map.get(&TypeId::of::<T>()))
+ .and_then(|boxed| (boxed.as_ref() as &dyn Any).downcast_ref::<T>())
+ }
+}
+
+impl<S> SwarmPlugins<S>
+where
+ S: 'static,
+{
+ /// Create a new empty set of plugins.
+ pub fn new() -> Self {
+ Self { map: None }
+ }
+
+ /// Add a new plugin to this set.
+ pub fn add<T: SwarmPlugin<S>>(&mut self, plugin: T) {
+ if self.map.is_none() {
+ self.map = Some(HashMap::with_hasher(BuildHasherDefault::default()));
+ }
+ self.map
+ .as_mut()
+ .unwrap()
+ .insert(TypeId::of::<T>(), Box::new(plugin));
+ }
+
+ /// 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<S> {
+ if self.map.is_none() {
+ return SwarmPluginStates { map: None };
+ }
+ let mut map = HashMap::with_hasher(BuildHasherDefault::default());
+ for (id, plugin) in self.map.unwrap().into_iter() {
+ map.insert(id, plugin.build());
+ }
+ SwarmPluginStates { map: Some(map) }
+ }
+}
+
+impl<S> IntoIterator for SwarmPluginStates<S> {
+ type Item = Box<dyn SwarmPluginState<S>>;
+ type IntoIter = std::vec::IntoIter<Self::Item>;
+
+ /// Iterate over the plugin states.
+ fn into_iter(self) -> Self::IntoIter {
+ self.map
+ .map(|map| map.into_values().collect::<Vec<_>>())
+ .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<S>: Send + Sync + SwarmPluginStateClone<S> + Any + 'static {
+ async fn handle(self: Box<Self>, event: SwarmEvent, swarm: Swarm<S>);
+}
+
+/// Swarm plugins can keep their own personal state ([`SwarmPluginState`]),
+/// listen to [`SwarmEvent`]s, and add new functions to [`Swarm`].
+pub trait SwarmPlugin<S>: Send + Sync + SwarmPluginClone<S> + Any + 'static {
+ fn build(&self) -> Box<dyn SwarmPluginState<S>>;
+}
+
+/// An internal trait that allows SwarmPluginState to be cloned.
+#[doc(hidden)]
+pub trait SwarmPluginStateClone<S> {
+ fn clone_box(&self) -> Box<dyn SwarmPluginState<S>>;
+}
+impl<T, S> SwarmPluginStateClone<S> for T
+where
+ T: 'static + SwarmPluginState<S> + Clone,
+{
+ fn clone_box(&self) -> Box<dyn SwarmPluginState<S>> {
+ Box::new(self.clone())
+ }
+}
+impl<S> Clone for Box<dyn SwarmPluginState<S>> {
+ fn clone(&self) -> Self {
+ self.clone_box()
+ }
+}
+
+/// An internal trait that allows SwarmPlugin to be cloned.
+#[doc(hidden)]
+pub trait SwarmPluginClone<S> {
+ fn clone_box(&self) -> Box<dyn SwarmPlugin<S>>;
+}
+impl<T, S> SwarmPluginClone<S> for T
+where
+ T: 'static + SwarmPlugin<S> + Clone,
+{
+ fn clone_box(&self) -> Box<dyn SwarmPlugin<S>> {
+ Box::new(self.clone())
+ }
+}
+impl<S> Clone for Box<dyn SwarmPlugin<S>> {
+ 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
index 942868e2..e50da728 100755..100644
--- 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() == "<py5> 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<State>,
+ 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() == "<py5> 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() == "<py5> hi" {
+ for (bot, _) in swarm {
+ bot.chat("hello").await?;
+ }
+ }
+ }
+ _ => {}
+ }
+ Ok(())
+}