aboutsummaryrefslogtreecommitdiff
path: root/azalea-client/src
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2022-11-27 16:25:07 -0600
committerGitHub <noreply@github.com>2022-11-27 16:25:07 -0600
commit631ed63dbdc7167df4de02a55b5c2ef1cea909e9 (patch)
tree104e567c332f2aeb30ea6acefef8c73f9b2f158b /azalea-client/src
parent962b9fcaae917c7e5bef718469fba31f6ff7c3cb (diff)
downloadazalea-drasl-631ed63dbdc7167df4de02a55b5c2ef1cea909e9.tar.xz
Swarm (#36)
* make azalea-pathfinder dir * start writing d* lite impl * more work on d* lite * work more on implementing d* lite * full d* lite impl * updated edges * add next() function * add NoPathError * why does dstar lite not work * fix d* lite implementation * make the test actually check the coords * replace while loop with if statement * fix clippy complaints * make W only have to be PartialOrd * fix PartialOrd issues * implement mtd* lite * add a test to mtd* lite * remove normal d* lite * make heuristic only take in one arg * add `success` function * Update README.md * evil black magic to make .entity not need dimension * start adding moves * slightly improve the vec3/position situation new macro that implements all the useful functions * moves stuff * make it compile * update deps in az-pathfinder * make it compile again * more pathfinding stuff * add Bot::look_at * replace EntityMut and EntityRef with just Entity * block pos pathfinding stuff * rename movedirection to walkdirection * execute path every tick * advance path * change az-pf version * make azalea_client keep plugin state * fix Plugins::get * why does it think there is air * start debugging incorrect air * update some From methods to use rem_euclid * start adding swarm * fix deadlock i still don't understand why it was happening but the solution was to keep the Client::player lock for shorter so it didn't overlap with the Client::dimension lock * make lookat actually work probably * fix going too fast * Update main.rs * make a thing immutable * direction_looking_at * fix rotations * import swarm in an example * fix stuff from merge * remove azalea_pathfinder import * delete azalea-pathfinder crate already in azalea::pathfinder module * swarms * start working on shared dimensions * Shared worlds work * start adding Swarm::add_account * add_account works * change "client" to "bot" in some places * Fix issues from merge * Update world.rs * add SwarmEvent::Disconnect(Account) * almost add SwarmEvent::Chat and new plugin system it panics rn * make plugins have to provide the State associated type * improve comments * make fn build slightly cleaner * fix SwarmEvent::Chat * change a println in bot/main.rs * Client::shutdown -> disconnect * polish fix clippy warnings + improve some docs a bit * fix shared worlds* *there's a bug that entities and bots will have their positions exaggerated because the relative movement packet is applied for every entity once per bot * i am being trolled by rust for some reason some stuff is really slow for literally no reason and it makes no sense i am going insane * make world an RwLock again * remove debug messages * fix skipping event ticks unfortunately now sending events is `.send().await?` instead of just `.send()` * fix deadlock + warnings * turns out my floor_mod impl was wrong and i32::rem_euclid has the correct behavior LOL * still errors with lots of bots * make swarm iter & fix new chunks not loading * improve docs * start fixing tests * fix all the tests except the examples i don't know how to exclude them from the tests * improve docs some more
Diffstat (limited to 'azalea-client/src')
-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
6 files changed, 432 insertions, 261 deletions
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()
}