diff options
Diffstat (limited to 'azalea-client/src')
| -rwxr-xr-x | azalea-client/src/chat.rs | 25 | ||||
| -rw-r--r-- | azalea-client/src/client.rs | 500 | ||||
| -rw-r--r--[-rwxr-xr-x] | azalea-client/src/lib.rs | 6 | ||||
| -rw-r--r--[-rwxr-xr-x] | azalea-client/src/movement.rs | 39 | ||||
| -rwxr-xr-x | azalea-client/src/player.rs | 17 | ||||
| -rw-r--r-- | azalea-client/src/plugins.rs | 106 |
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() } |
