diff options
| author | mat <27899617+mat-1@users.noreply.github.com> | 2022-11-27 16:25:07 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-11-27 16:25:07 -0600 |
| commit | 631ed63dbdc7167df4de02a55b5c2ef1cea909e9 (patch) | |
| tree | 104e567c332f2aeb30ea6acefef8c73f9b2f158b /azalea-client/src/client.rs | |
| parent | 962b9fcaae917c7e5bef718469fba31f6ff7c3cb (diff) | |
| download | azalea-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/client.rs')
| -rw-r--r-- | azalea-client/src/client.rs | 500 |
1 files changed, 305 insertions, 195 deletions
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, |
