diff options
| author | mat <27899617+mat-1@users.noreply.github.com> | 2022-06-25 05:09:26 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-06-25 05:09:26 +0000 |
| commit | 7d3e57763e32ac9cf94180b1c714704cfbc3034d (patch) | |
| tree | 2dcfe72bf09a42f6614f9dc988dc0254162ea0bf /azalea-client/src | |
| parent | 69c47eda4c496b13dadd80976bffd2fab7ea5894 (diff) | |
| parent | ca7067e173129f3044ebc8c77634f06da29a086e (diff) | |
| download | azalea-drasl-7d3e57763e32ac9cf94180b1c714704cfbc3034d.tar.xz | |
Merge pull request #10 from mat-1/azalea-entity
azalea-entity
Diffstat (limited to 'azalea-client/src')
| -rw-r--r-- | azalea-client/src/account.rs | 20 | ||||
| -rw-r--r-- | azalea-client/src/client.rs | 673 | ||||
| -rwxr-xr-x | azalea-client/src/connect.rs | 477 | ||||
| -rw-r--r-- | azalea-client/src/entity.rs | 8 | ||||
| -rwxr-xr-x | azalea-client/src/lib.rs | 9 | ||||
| -rw-r--r-- | azalea-client/src/movement.rs | 45 | ||||
| -rw-r--r-- | azalea-client/src/player.rs | 26 |
7 files changed, 766 insertions, 492 deletions
diff --git a/azalea-client/src/account.rs b/azalea-client/src/account.rs new file mode 100644 index 00000000..56f4918a --- /dev/null +++ b/azalea-client/src/account.rs @@ -0,0 +1,20 @@ +//! Connect to Minecraft servers. + +use crate::Client; +use azalea_protocol::ServerAddress; + +/// Something that can join Minecraft servers. +pub struct Account { + pub username: String, +} +impl Account { + pub fn offline(username: &str) -> Self { + Self { + username: username.to_string(), + } + } + + pub async fn join(&self, address: &ServerAddress) -> Result<Client, String> { + Client::join(self, address).await + } +} diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs new file mode 100644 index 00000000..dfefe4ad --- /dev/null +++ b/azalea-client/src/client.rs @@ -0,0 +1,673 @@ +use crate::{Account, Player}; +use azalea_auth::game_profile::GameProfile; +use azalea_core::{ChunkPos, EntityPos, PositionDelta, PositionDeltaTrait, ResourceLocation}; +use azalea_entity::Entity; +use azalea_protocol::{ + connect::{GameConnection, HandshakeConnection}, + packets::{ + game::{ + clientbound_player_chat_packet::ClientboundPlayerChatPacket, + clientbound_system_chat_packet::ClientboundSystemChatPacket, + serverbound_accept_teleportation_packet::ServerboundAcceptTeleportationPacket, + serverbound_custom_payload_packet::ServerboundCustomPayloadPacket, + serverbound_keep_alive_packet::ServerboundKeepAlivePacket, + serverbound_move_player_packet_pos_rot::ServerboundMovePlayerPacketPosRot, GamePacket, + }, + handshake::client_intention_packet::ClientIntentionPacket, + login::{ + serverbound_hello_packet::ServerboundHelloPacket, + serverbound_key_packet::{NonceOrSaltSignature, ServerboundKeyPacket}, + LoginPacket, + }, + ConnectionProtocol, PROTOCOL_VERSION, + }, + resolver, ServerAddress, +}; +use azalea_world::World; +use owning_ref::OwningRef; +use std::{ + fmt::Debug, + sync::{Arc, Mutex}, +}; +use tokio::{ + sync::mpsc::{self, UnboundedReceiver, UnboundedSender}, + time::{self, MissedTickBehavior}, +}; + +#[derive(Default)] +pub struct ClientState { + pub player: Player, + pub world: Option<World>, +} + +#[derive(Debug, Clone)] +pub enum Event { + Login, + Chat(ChatPacket), + /// A game tick, happens 20 times per second. + GameTick, +} + +#[derive(Debug, Clone)] +pub enum ChatPacket { + System(ClientboundSystemChatPacket), + Player(Box<ClientboundPlayerChatPacket>), +} + +// impl ChatPacket { +// pub fn message(&self) -> &str { +// match self { +// ChatPacket::System(p) => &p.content, +// ChatPacket::Player(p) => &p.message, +// } +// } +// } + +/// A player that you can control that is currently in a Minecraft server. +pub struct Client { + event_receiver: UnboundedReceiver<Event>, + game_profile: GameProfile, + pub conn: Arc<tokio::sync::Mutex<GameConnection>>, + pub state: Arc<Mutex<ClientState>>, + // game_loop +} + +/// Whether we should ignore errors when decoding packets. +const IGNORE_ERRORS: bool = !cfg!(debug_assertions); + +#[derive(Debug)] +struct HandleError(String); + +impl Client { + /// Connect to a Minecraft server with an account. + pub async fn join(account: &Account, address: &ServerAddress) -> Result<Self, String> { + let resolved_address = resolver::resolve_address(address).await?; + + let mut conn = HandshakeConnection::new(&resolved_address).await?; + + // handshake + conn.write( + ClientIntentionPacket { + protocol_version: PROTOCOL_VERSION, + hostname: address.host.clone(), + port: address.port, + intention: ConnectionProtocol::Login, + } + .get(), + ) + .await; + let mut conn = conn.login(); + + // login + conn.write( + ServerboundHelloPacket { + username: account.username.clone(), + public_key: None, + } + .get(), + ) + .await; + + let (conn, game_profile) = loop { + let packet_result = conn.read().await; + match packet_result { + Ok(packet) => match packet { + LoginPacket::ClientboundHelloPacket(p) => { + println!("Got encryption request"); + let e = azalea_crypto::encrypt(&p.public_key, &p.nonce).unwrap(); + + // TODO: authenticate with the server here (authenticateServer) + + conn.write( + ServerboundKeyPacket { + nonce_or_salt_signature: NonceOrSaltSignature::Nonce( + e.encrypted_nonce, + ), + key_bytes: e.encrypted_public_key, + } + .get(), + ) + .await; + conn.set_encryption_key(e.secret_key); + } + LoginPacket::ClientboundLoginCompressionPacket(p) => { + println!("Got compression request {:?}", p.compression_threshold); + conn.set_compression_threshold(p.compression_threshold); + } + LoginPacket::ClientboundGameProfilePacket(p) => { + println!("Got profile {:?}", p.game_profile); + break (conn.game(), p.game_profile); + } + LoginPacket::ClientboundLoginDisconnectPacket(p) => { + println!("Got disconnect {:?}", p); + } + LoginPacket::ClientboundCustomQueryPacket(p) => { + println!("Got custom query {:?}", p); + } + _ => panic!("Unexpected packet {:?}", packet), + }, + Err(e) => { + panic!("Error: {:?}", e); + } + } + }; + + let conn = Arc::new(tokio::sync::Mutex::new(conn)); + + let (tx, rx) = mpsc::unbounded_channel(); + + // we got the GameConnection, so the server is now connected :) + let client = Client { + game_profile: game_profile.clone(), + event_receiver: rx, + conn: conn.clone(), + state: Arc::new(Mutex::new(ClientState::default())), + }; + + // just start up the game loop and we're ready! + + let game_loop_state = client.state.clone(); + + // 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 + tokio::spawn(Self::protocol_loop( + conn.clone(), + tx.clone(), + game_loop_state.clone(), + game_profile.clone(), + )); + tokio::spawn(Self::game_tick_loop(conn, tx, game_loop_state)); + + Ok(client) + } + + async fn protocol_loop( + conn: Arc<tokio::sync::Mutex<GameConnection>>, + tx: UnboundedSender<Event>, + state: Arc<Mutex<ClientState>>, + game_profile: GameProfile, + ) { + loop { + let r = conn.lock().await.read().await; + match r { + Ok(packet) => { + match Self::handle(&packet, &tx, &state, &conn, &game_profile).await { + Ok(_) => {} + Err(e) => { + println!("Error handling packet: {:?}", e); + if IGNORE_ERRORS { + continue; + } else { + panic!("Error handling packet: {:?}", e); + } + } + } + } + Err(e) => { + if IGNORE_ERRORS { + println!("Error: {:?}", e); + if e == "length wider than 21-bit" { + panic!(); + } + } else { + panic!("Error: {:?}", e); + } + } + }; + } + } + + async fn handle( + packet: &GamePacket, + tx: &UnboundedSender<Event>, + state: &Arc<Mutex<ClientState>>, + conn: &Arc<tokio::sync::Mutex<GameConnection>>, + game_profile: &GameProfile, + ) -> Result<(), HandleError> { + match packet { + GamePacket::ClientboundLoginPacket(p) => { + println!("Got login packet {:?}", p); + + { + let mut state_lock = state.lock()?; + + // // write p into login.txt + // std::io::Write::write_all( + // &mut std::fs::File::create("login.txt").unwrap(), + // format!("{:#?}", p).as_bytes(), + // ) + // .unwrap(); + + // TODO: have registry_holder be a struct because this sucks rn + // best way would be to add serde support to azalea-nbt + + let registry_holder = p + .registry_holder + .as_compound() + .expect("Registry holder is not a compound") + .get("") + .expect("No \"\" tag") + .as_compound() + .expect("\"\" tag is not a compound"); + let dimension_types = registry_holder + .get("minecraft:dimension_type") + .expect("No dimension_type tag") + .as_compound() + .expect("dimension_type is not a compound") + .get("value") + .expect("No dimension_type value") + .as_list() + .expect("dimension_type value is not a list"); + let dimension_type = dimension_types + .iter() + .find(|t| { + t.as_compound() + .expect("dimension_type value is not a compound") + .get("name") + .expect("No name tag") + .as_string() + .expect("name is not a string") + == p.dimension_type.to_string() + }) + .unwrap_or_else(|| { + panic!("No dimension_type with name {}", p.dimension_type) + }) + .as_compound() + .unwrap() + .get("element") + .expect("No element tag") + .as_compound() + .expect("element is not a compound"); + let height = (*dimension_type + .get("height") + .expect("No height tag") + .as_int() + .expect("height tag is not an int")) + .try_into() + .expect("height is not a u32"); + let min_y = *dimension_type + .get("min_y") + .expect("No min_y tag") + .as_int() + .expect("min_y tag is not an int"); + + // the 16 here is our render distance + // i'll make this an actual setting later + state_lock.world = Some(World::new(16, height, min_y)); + + let entity = Entity::new(p.player_id, game_profile.uuid, EntityPos::default()); + state_lock + .world + .as_mut() + .expect("World doesn't exist! We should've gotten a login packet by now.") + .add_entity(entity); + + state_lock.player.set_entity_id(p.player_id); + } + + conn.lock() + .await + .write( + ServerboundCustomPayloadPacket { + identifier: ResourceLocation::new("brand").unwrap(), + // they don't have to know :) + data: "vanilla".into(), + } + .get(), + ) + .await; + + tx.send(Event::Login).unwrap(); + } + GamePacket::ClientboundUpdateViewDistancePacket(p) => { + println!("Got view distance packet {:?}", p); + } + GamePacket::ClientboundCustomPayloadPacket(p) => { + println!("Got custom payload packet {:?}", p); + } + GamePacket::ClientboundChangeDifficultyPacket(p) => { + println!("Got difficulty packet {:?}", p); + } + GamePacket::ClientboundDeclareCommandsPacket(_p) => { + println!("Got declare commands packet"); + } + GamePacket::ClientboundPlayerAbilitiesPacket(p) => { + println!("Got player abilities packet {:?}", p); + } + GamePacket::ClientboundSetCarriedItemPacket(p) => { + println!("Got set carried item packet {:?}", p); + } + GamePacket::ClientboundUpdateTagsPacket(_p) => { + println!("Got update tags packet"); + } + GamePacket::ClientboundDisconnectPacket(p) => { + println!("Got disconnect packet {:?}", p); + } + GamePacket::ClientboundUpdateRecipesPacket(_p) => { + println!("Got update recipes packet"); + } + GamePacket::ClientboundEntityEventPacket(_p) => { + // println!("Got entity event packet {:?}", p); + } + GamePacket::ClientboundRecipePacket(_p) => { + println!("Got recipe packet"); + } + GamePacket::ClientboundPlayerPositionPacket(p) => { + // TODO: reply with teleport confirm + println!("Got player position packet {:?}", p); + + let (new_pos, y_rot, x_rot) = { + let mut state_lock = state.lock()?; + let player_entity_id = state_lock.player.entity_id; + let world = state_lock.world.as_mut().unwrap(); + let player_entity = world + .mut_entity_by_id(player_entity_id) + .expect("Player entity doesn't exist"); + let delta_movement = &player_entity.delta; + + let is_x_relative = p.relative_arguments.x; + let is_y_relative = p.relative_arguments.y; + let is_z_relative = p.relative_arguments.z; + + let (delta_x, new_pos_x) = if is_x_relative { + player_entity.old_pos.x += p.x; + (delta_movement.x(), player_entity.pos().x + p.x) + } else { + player_entity.old_pos.x = p.x; + (0.0, p.x) + }; + let (delta_y, new_pos_y) = if is_y_relative { + player_entity.old_pos.y += p.y; + (delta_movement.y(), player_entity.pos().y + p.y) + } else { + player_entity.old_pos.y = p.y; + (0.0, p.y) + }; + let (delta_z, new_pos_z) = if is_z_relative { + player_entity.old_pos.z += p.z; + (delta_movement.z(), player_entity.pos().z + p.z) + } else { + player_entity.old_pos.z = p.z; + (0.0, p.z) + }; + + let mut y_rot = p.y_rot; + let mut x_rot = p.x_rot; + if p.relative_arguments.x_rot { + y_rot += player_entity.x_rot; + } + if p.relative_arguments.y_rot { + x_rot += player_entity.y_rot; + } + + player_entity.delta = PositionDelta { + xa: delta_x, + ya: delta_y, + za: delta_z, + }; + player_entity.set_rotation(y_rot, x_rot); + // TODO: minecraft sets "xo", "yo", and "zo" here but idk what that means + // so investigate that ig + let new_pos = EntityPos { + x: new_pos_x, + y: new_pos_y, + z: new_pos_z, + }; + world + .move_entity(player_entity_id, new_pos) + .expect("The player entity should always exist"); + + (new_pos, y_rot, x_rot) + }; + + let mut conn_lock = conn.lock().await; + conn_lock + .write(ServerboundAcceptTeleportationPacket { id: p.id }.get()) + .await; + conn_lock + .write( + ServerboundMovePlayerPacketPosRot { + x: new_pos.x, + y: new_pos.y, + z: new_pos.z, + y_rot, + x_rot, + // this is always false + on_ground: false, + } + .get(), + ) + .await; + } + GamePacket::ClientboundPlayerInfoPacket(p) => { + println!("Got player info packet {:?}", p); + } + GamePacket::ClientboundSetChunkCacheCenterPacket(p) => { + println!("Got chunk cache center packet {:?}", p); + state + .lock()? + .world + .as_mut() + .unwrap() + .update_view_center(&ChunkPos::new(p.x, p.z)); + } + GamePacket::ClientboundLevelChunkWithLightPacket(p) => { + println!("Got chunk with light packet {} {}", p.x, p.z); + let pos = ChunkPos::new(p.x, p.z); + // let chunk = Chunk::read_with_world_height(&mut p.chunk_data); + // println("chunk {:?}") + state + .lock()? + .world + .as_mut() + .expect("World doesn't exist! We should've gotten a login packet by now.") + .replace_with_packet_data(&pos, &mut p.chunk_data.data.as_slice()) + .unwrap(); + } + GamePacket::ClientboundLightUpdatePacket(p) => { + println!("Got light update packet {:?}", p); + } + GamePacket::ClientboundAddEntityPacket(p) => { + println!("Got add entity packet {:?}", p); + let entity = Entity::from(p); + state + .lock()? + .world + .as_mut() + .expect("World doesn't exist! We should've gotten a login packet by now.") + .add_entity(entity); + } + GamePacket::ClientboundSetEntityDataPacket(_p) => { + // println!("Got set entity data packet {:?}", p); + } + GamePacket::ClientboundUpdateAttributesPacket(_p) => { + // println!("Got update attributes packet {:?}", p); + } + GamePacket::ClientboundEntityVelocityPacket(_p) => { + // println!("Got entity velocity packet {:?}", p); + } + GamePacket::ClientboundSetEntityLinkPacket(p) => { + println!("Got set entity link packet {:?}", p); + } + GamePacket::ClientboundAddPlayerPacket(p) => { + println!("Got add player packet {:?}", p); + let entity = Entity::from(p); + state + .lock()? + .world + .as_mut() + .expect("World doesn't exist! We should've gotten a login packet by now.") + .add_entity(entity); + } + GamePacket::ClientboundInitializeBorderPacket(p) => { + println!("Got initialize border packet {:?}", p); + } + GamePacket::ClientboundSetTimePacket(p) => { + println!("Got set time packet {:?}", p); + } + GamePacket::ClientboundSetDefaultSpawnPositionPacket(p) => { + println!("Got set default spawn position packet {:?}", p); + } + GamePacket::ClientboundContainerSetContentPacket(p) => { + println!("Got container set content packet {:?}", p); + } + GamePacket::ClientboundSetHealthPacket(p) => { + println!("Got set health packet {:?}", p); + } + GamePacket::ClientboundSetExperiencePacket(p) => { + println!("Got set experience packet {:?}", p); + } + GamePacket::ClientboundTeleportEntityPacket(p) => { + let mut state_lock = state.lock()?; + let world = state_lock.world.as_mut().unwrap(); + + world.move_entity( + p.id, + EntityPos { + x: p.x, + y: p.y, + z: p.z, + }, + )?; + } + GamePacket::ClientboundUpdateAdvancementsPacket(p) => { + println!("Got update advancements packet {:?}", p); + } + GamePacket::ClientboundRotateHeadPacket(_p) => { + // println!("Got rotate head packet {:?}", p); + } + GamePacket::ClientboundMoveEntityPosPacket(p) => { + let mut state_lock = state.lock()?; + let world = state_lock.world.as_mut().unwrap(); + + world.move_entity_with_delta(p.entity_id, &p.delta)?; + } + GamePacket::ClientboundMoveEntityPosRotPacket(p) => { + let mut state_lock = state.lock()?; + let world = state_lock.world.as_mut().unwrap(); + + world.move_entity_with_delta(p.entity_id, &p.delta)?; + } + GamePacket::ClientboundMoveEntityRotPacket(p) => { + println!("Got move entity rot packet {:?}", p); + } + GamePacket::ClientboundKeepAlivePacket(p) => { + println!("Got keep alive packet {:?}", p); + conn.lock() + .await + .write(ServerboundKeepAlivePacket { id: p.id }.get()) + .await; + } + GamePacket::ClientboundRemoveEntitiesPacket(p) => { + println!("Got remove entities packet {:?}", p); + } + GamePacket::ClientboundPlayerChatPacket(p) => { + println!("Got player chat packet {:?}", p); + tx.send(Event::Chat(ChatPacket::Player(Box::new(p.clone())))) + .unwrap(); + } + GamePacket::ClientboundSystemChatPacket(p) => { + println!("Got system chat packet {:?}", p); + tx.send(Event::Chat(ChatPacket::System(p.clone()))).unwrap(); + } + GamePacket::ClientboundSoundPacket(p) => { + println!("Got sound packet {:?}", p); + } + GamePacket::ClientboundLevelEventPacket(p) => { + println!("Got level event packet {:?}", p); + } + GamePacket::ClientboundBlockUpdatePacket(p) => { + println!("Got block update packet {:?}", p); + // TODO: update world + } + GamePacket::ClientboundAnimatePacket(p) => { + println!("Got animate packet {:?}", p); + } + GamePacket::ClientboundSectionBlocksUpdatePacket(p) => { + println!("Got section blocks update packet {:?}", p); + // TODO: update world + } + GamePacket::ClientboundGameEventPacket(p) => { + println!("Got game event packet {:?}", p); + } + GamePacket::ClientboundLevelParticlesPacket(p) => { + println!("Got level particles packet {:?}", p); + } + GamePacket::ClientboundServerDataPacket(p) => { + println!("Got server data packet {:?}", p); + } + GamePacket::ClientboundSetEquipmentPacket(p) => { + println!("Got set equipment packet {:?}", p); + } + GamePacket::ClientboundUpdateMobEffectPacket(p) => { + println!("Got update mob effect packet {:?}", p); + } + _ => panic!("Unexpected packet {:?}", packet), + } + + Ok(()) + } + + pub async fn next(&mut self) -> Option<Event> { + self.event_receiver.recv().await + } + + /// Runs game_tick every 50 milliseconds. + async fn game_tick_loop( + conn: Arc<tokio::sync::Mutex<GameConnection>>, + tx: UnboundedSender<Event>, + state: Arc<Mutex<ClientState>>, + ) { + 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); + loop { + game_tick_interval.tick().await; + Self::game_tick(&conn, &tx, &state).await; + } + } + + /// Runs every 50 milliseconds. + async fn game_tick( + conn: &Arc<tokio::sync::Mutex<GameConnection>>, + tx: &UnboundedSender<Event>, + state: &Arc<Mutex<ClientState>>, + ) { + if state.lock().unwrap().world.is_none() { + return; + } + tx.send(Event::GameTick).unwrap(); + } + + /// Gets the `World` the client is in. + /// + /// This is basically a shortcut for `client.state.lock().unwrap().world.as_ref().unwrap()`. + /// If the client hasn't received a login packet yet, this will panic. + pub fn world(&self) -> OwningRef<std::sync::MutexGuard<ClientState>, World> { + let state_lock: std::sync::MutexGuard<ClientState> = self.state.lock().unwrap(); + let state_lock_ref = OwningRef::new(state_lock); + state_lock_ref.map(|state| state.world.as_ref().expect("World doesn't exist!")) + } + + /// Gets the `Player` struct for our player. + /// + /// This is basically a shortcut for `client.state.lock().unwrap().player`. + pub fn player(&self) -> OwningRef<std::sync::MutexGuard<ClientState>, Player> { + let state_lock: std::sync::MutexGuard<ClientState> = self.state.lock().unwrap(); + let state_lock_ref = OwningRef::new(state_lock); + state_lock_ref.map(|state| &state.player) + } +} + +impl<T> From<std::sync::PoisonError<T>> for HandleError { + fn from(e: std::sync::PoisonError<T>) -> Self { + HandleError(e.to_string()) + } +} + +impl From<String> for HandleError { + fn from(e: String) -> Self { + HandleError(e) + } +} diff --git a/azalea-client/src/connect.rs b/azalea-client/src/connect.rs deleted file mode 100755 index 1551ca69..00000000 --- a/azalea-client/src/connect.rs +++ /dev/null @@ -1,477 +0,0 @@ -use crate::Player; -use azalea_core::{resource_location::ResourceLocation, ChunkPos, EntityPos}; -use azalea_protocol::{ - connect::{GameConnection, HandshakeConnection}, - packets::{ - game::{ - clientbound_player_chat_packet::ClientboundPlayerChatPacket, - clientbound_system_chat_packet::ClientboundSystemChatPacket, - serverbound_custom_payload_packet::ServerboundCustomPayloadPacket, - serverbound_keep_alive_packet::ServerboundKeepAlivePacket, GamePacket, - }, - handshake::client_intention_packet::ClientIntentionPacket, - login::{ - serverbound_hello_packet::ServerboundHelloPacket, - serverbound_key_packet::{NonceOrSaltSignature, ServerboundKeyPacket}, - LoginPacket, - }, - ConnectionProtocol, PROTOCOL_VERSION, - }, - resolver, ServerAddress, -}; -use azalea_world::{ChunkStorage, World}; -use std::{fmt::Debug, sync::Arc}; -use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; -use tokio::sync::Mutex; - -///! Connect to Minecraft servers. - -/// Something that can join Minecraft servers. -pub struct Account { - username: String, -} - -#[derive(Default)] -pub struct ClientState { - pub player: Player, - pub world: Option<World>, -} - -/// A player that you can control that is currently in a Minecraft server. -pub struct Client { - event_receiver: UnboundedReceiver<Event>, - pub conn: Arc<Mutex<GameConnection>>, - pub state: Arc<Mutex<ClientState>>, - // game_loop -} - -#[derive(Debug, Clone)] -pub enum ChatPacket { - System(ClientboundSystemChatPacket), - Player(ClientboundPlayerChatPacket), -} - -// impl ChatPacket { -// pub fn message(&self) -> &str { -// match self { -// ChatPacket::System(p) => &p.content, -// ChatPacket::Player(p) => &p.message, -// } -// } -// } - -#[derive(Debug, Clone)] -pub enum Event { - Login, - Chat(ChatPacket), -} - -/// Whether we should ignore errors when decoding packets. -const IGNORE_ERRORS: bool = false; - -impl Client { - async fn join(account: &Account, address: &ServerAddress) -> Result<Self, String> { - let resolved_address = resolver::resolve_address(address).await?; - - let mut conn = HandshakeConnection::new(&resolved_address).await?; - - // handshake - conn.write( - ClientIntentionPacket { - protocol_version: PROTOCOL_VERSION, - hostname: address.host.clone(), - port: address.port, - intention: ConnectionProtocol::Login, - } - .get(), - ) - .await; - let mut conn = conn.login(); - - // login - conn.write( - ServerboundHelloPacket { - username: account.username.clone(), - public_key: None, - } - .get(), - ) - .await; - - let conn = loop { - let packet_result = conn.read().await; - match packet_result { - Ok(packet) => match packet { - LoginPacket::ClientboundHelloPacket(p) => { - println!("Got encryption request"); - let e = azalea_crypto::encrypt(&p.public_key, &p.nonce).unwrap(); - - // TODO: authenticate with the server here (authenticateServer) - - conn.write( - ServerboundKeyPacket { - nonce_or_salt_signature: NonceOrSaltSignature::Nonce( - e.encrypted_nonce, - ), - key_bytes: e.encrypted_public_key, - } - .get(), - ) - .await; - conn.set_encryption_key(e.secret_key); - } - LoginPacket::ClientboundLoginCompressionPacket(p) => { - println!("Got compression request {:?}", p.compression_threshold); - conn.set_compression_threshold(p.compression_threshold); - } - LoginPacket::ClientboundGameProfilePacket(p) => { - println!("Got profile {:?}", p.game_profile); - break conn.game(); - } - LoginPacket::ClientboundLoginDisconnectPacket(p) => { - println!("Got disconnect {:?}", p); - } - LoginPacket::ClientboundCustomQueryPacket(p) => { - println!("Got custom query {:?}", p); - } - _ => panic!("Unexpected packet {:?}", packet), - }, - Err(e) => { - panic!("Error: {:?}", e); - } - } - }; - - let conn = Arc::new(Mutex::new(conn)); - - let (tx, rx) = mpsc::unbounded_channel(); - - // we got the GameConnection, so the server is now connected :) - let client = Client { - event_receiver: rx, - conn: conn.clone(), - state: Arc::new(Mutex::new(ClientState::default())), - }; - // let client = Arc::new(Mutex::new(client)); - // let weak_client = Arc::<_>::downgrade(&client); - - // just start up the game loop and we're ready! - // tokio::spawn(Self::game_loop(conn, tx, handler, state)) - - let game_loop_state = client.state.clone(); - - tokio::spawn(Self::game_loop(conn, tx, game_loop_state)); - - Ok(client) - } - - async fn game_loop( - conn: Arc<Mutex<GameConnection>>, - tx: UnboundedSender<Event>, - state: Arc<Mutex<ClientState>>, - ) { - loop { - let r = conn.lock().await.read().await; - match r { - Ok(packet) => Self::handle(&packet, &tx, &state, &conn).await, - Err(e) => { - if IGNORE_ERRORS { - println!("Error: {:?}", e); - if e == "length wider than 21-bit" { - panic!(); - } - } else { - panic!("Error: {:?}", e); - } - } - }; - } - } - - async fn handle( - packet: &GamePacket, - tx: &UnboundedSender<Event>, - state: &Arc<Mutex<ClientState>>, - conn: &Arc<Mutex<GameConnection>>, - ) { - match packet { - GamePacket::ClientboundLoginPacket(p) => { - println!("Got login packet {:?}", p); - - let mut state = state.lock().await; - - // // write p into login.txt - // std::io::Write::write_all( - // &mut std::fs::File::create("login.txt").unwrap(), - // format!("{:#?}", p).as_bytes(), - // ) - // .unwrap(); - - state.player.entity.id = p.player_id; - - // TODO: have registry_holder be a struct because this sucks rn - // best way would be to add serde support to azalea-nbt - - let registry_holder = p - .registry_holder - .as_compound() - .expect("Registry holder is not a compound") - .get("") - .expect("No \"\" tag") - .as_compound() - .expect("\"\" tag is not a compound"); - let dimension_types = registry_holder - .get("minecraft:dimension_type") - .expect("No dimension_type tag") - .as_compound() - .expect("dimension_type is not a compound") - .get("value") - .expect("No dimension_type value") - .as_list() - .expect("dimension_type value is not a list"); - let dimension_type = dimension_types - .iter() - .find(|t| { - t.as_compound() - .expect("dimension_type value is not a compound") - .get("name") - .expect("No name tag") - .as_string() - .expect("name is not a string") - == p.dimension_type.to_string() - }) - .expect(&format!("No dimension_type with name {}", p.dimension_type)) - .as_compound() - .unwrap() - .get("element") - .expect("No element tag") - .as_compound() - .expect("element is not a compound"); - let height = (*dimension_type - .get("height") - .expect("No height tag") - .as_int() - .expect("height tag is not an int")) - .try_into() - .expect("height is not a u32"); - let min_y = (*dimension_type - .get("min_y") - .expect("No min_y tag") - .as_int() - .expect("min_y tag is not an int")) - .try_into() - .expect("min_y is not an i32"); - - state.world = Some(World { - height, - min_y, - storage: ChunkStorage::new(16), - }); - - conn.lock() - .await - .write( - ServerboundCustomPayloadPacket { - identifier: ResourceLocation::new("brand").unwrap(), - // they don't have to know :) - data: "vanilla".into(), - } - .get(), - ) - .await; - - tx.send(Event::Login).unwrap(); - } - GamePacket::ClientboundUpdateViewDistancePacket(p) => { - println!("Got view distance packet {:?}", p); - } - GamePacket::ClientboundCustomPayloadPacket(p) => { - println!("Got custom payload packet {:?}", p); - } - GamePacket::ClientboundChangeDifficultyPacket(p) => { - println!("Got difficulty packet {:?}", p); - } - GamePacket::ClientboundDeclareCommandsPacket(_p) => { - println!("Got declare commands packet"); - } - GamePacket::ClientboundPlayerAbilitiesPacket(p) => { - println!("Got player abilities packet {:?}", p); - } - GamePacket::ClientboundSetCarriedItemPacket(p) => { - println!("Got set carried item packet {:?}", p); - } - GamePacket::ClientboundUpdateTagsPacket(_p) => { - println!("Got update tags packet"); - } - GamePacket::ClientboundDisconnectPacket(p) => { - println!("Got disconnect packet {:?}", p); - } - GamePacket::ClientboundUpdateRecipesPacket(_p) => { - println!("Got update recipes packet"); - } - GamePacket::ClientboundEntityEventPacket(p) => { - // println!("Got entity event packet {:?}", p); - } - GamePacket::ClientboundRecipePacket(_p) => { - println!("Got recipe packet"); - } - GamePacket::ClientboundPlayerPositionPacket(p) => { - // TODO: reply with teleport confirm - println!("Got player position packet {:?}", p); - } - GamePacket::ClientboundPlayerInfoPacket(p) => { - println!("Got player info packet {:?}", p); - } - GamePacket::ClientboundSetChunkCacheCenterPacket(p) => { - println!("Got chunk cache center packet {:?}", p); - state - .lock() - .await - .world - .as_mut() - .unwrap() - .update_view_center(&ChunkPos::new(p.x, p.z)); - } - GamePacket::ClientboundLevelChunkWithLightPacket(p) => { - println!("Got chunk with light packet {} {}", p.x, p.z); - let pos = ChunkPos::new(p.x, p.z); - // let chunk = Chunk::read_with_world_height(&mut p.chunk_data); - // println("chunk {:?}") - state - .lock() - .await - .world - .as_mut() - .expect("World doesn't exist! We should've gotten a login packet by now.") - .replace_with_packet_data(&pos, &mut p.chunk_data.data.as_slice()) - .unwrap(); - } - GamePacket::ClientboundLightUpdatePacket(p) => { - println!("Got light update packet {:?}", p); - } - GamePacket::ClientboundAddEntityPacket(p) => { - println!("Got add entity packet {:?}", p); - let pos = EntityPos { - x: p.x, - y: p.y, - z: p.z, - }; - } - GamePacket::ClientboundSetEntityDataPacket(p) => { - // println!("Got set entity data packet {:?}", p); - } - GamePacket::ClientboundUpdateAttributesPacket(p) => { - // println!("Got update attributes packet {:?}", p); - } - GamePacket::ClientboundEntityVelocityPacket(p) => { - // println!("Got entity velocity packet {:?}", p); - } - GamePacket::ClientboundSetEntityLinkPacket(p) => { - println!("Got set entity link packet {:?}", p); - } - GamePacket::ClientboundAddPlayerPacket(p) => { - println!("Got add player packet {:?}", p); - } - GamePacket::ClientboundInitializeBorderPacket(p) => { - println!("Got initialize border packet {:?}", p); - } - GamePacket::ClientboundSetTimePacket(p) => { - println!("Got set time packet {:?}", p); - } - GamePacket::ClientboundSetDefaultSpawnPositionPacket(p) => { - println!("Got set default spawn position packet {:?}", p); - } - GamePacket::ClientboundContainerSetContentPacket(p) => { - println!("Got container set content packet {:?}", p); - } - GamePacket::ClientboundSetHealthPacket(p) => { - println!("Got set health packet {:?}", p); - } - GamePacket::ClientboundSetExperiencePacket(p) => { - println!("Got set experience packet {:?}", p); - } - GamePacket::ClientboundTeleportEntityPacket(p) => { - // println!("Got teleport entity packet {:?}", p); - } - GamePacket::ClientboundUpdateAdvancementsPacket(p) => { - println!("Got update advancements packet {:?}", p); - } - GamePacket::ClientboundRotateHeadPacket(p) => { - // println!("Got rotate head packet {:?}", p); - } - GamePacket::ClientboundMoveEntityPosPacket(p) => { - // println!("Got move entity pos packet {:?}", p); - } - GamePacket::ClientboundMoveEntityPosRotPacket(p) => { - // println!("Got move entity pos rot packet {:?}", p); - } - GamePacket::ClientboundMoveEntityRotPacket(p) => { - println!("Got move entity rot packet {:?}", p); - } - GamePacket::ClientboundKeepAlivePacket(p) => { - println!("Got keep alive packet {:?}", p); - conn.lock() - .await - .write(ServerboundKeepAlivePacket { id: p.id }.get()) - .await; - } - GamePacket::ClientboundRemoveEntitiesPacket(p) => { - println!("Got remove entities packet {:?}", p); - } - GamePacket::ClientboundPlayerChatPacket(p) => { - println!("Got player chat packet {:?}", p); - tx.send(Event::Chat(ChatPacket::Player(p.clone()))).unwrap(); - } - GamePacket::ClientboundSystemChatPacket(p) => { - println!("Got system chat packet {:?}", p); - tx.send(Event::Chat(ChatPacket::System(p.clone()))).unwrap(); - } - GamePacket::ClientboundSoundPacket(p) => { - println!("Got sound packet {:?}", p); - } - GamePacket::ClientboundLevelEventPacket(p) => { - println!("Got level event packet {:?}", p); - } - GamePacket::ClientboundBlockUpdatePacket(p) => { - println!("Got block update packet {:?}", p); - // TODO: update world - } - GamePacket::ClientboundAnimatePacket(p) => { - println!("Got animate packet {:?}", p); - } - GamePacket::ClientboundSectionBlocksUpdatePacket(p) => { - println!("Got section blocks update packet {:?}", p); - // TODO: update world - } - GamePacket::ClientboundGameEventPacket(p) => { - println!("Got game event packet {:?}", p); - } - GamePacket::ClientboundLevelParticlesPacket(p) => { - println!("Got level particles packet {:?}", p); - } - GamePacket::ClientboundServerDataPacket(p) => { - println!("Got server data packet {:?}", p); - } - GamePacket::ClientboundSetEquipmentPacket(p) => { - println!("Got set equipment packet {:?}", p); - } - _ => panic!("Unexpected packet {:?}", packet), - } - } - - pub async fn next(&mut self) -> Option<Event> { - self.event_receiver.recv().await - } -} - -impl Account { - pub fn offline(username: &str) -> Self { - Self { - username: username.to_string(), - } - } - - pub async fn join(&self, address: &ServerAddress) -> Result<Client, String> { - Client::join(self, address).await - } -} diff --git a/azalea-client/src/entity.rs b/azalea-client/src/entity.rs deleted file mode 100644 index d91f556f..00000000 --- a/azalea-client/src/entity.rs +++ /dev/null @@ -1,8 +0,0 @@ -use azalea_core::EntityPos; - -#[derive(Default, Debug)] -pub struct Entity { - /// The incremental numerical id of the entity. - pub id: u32, - pub pos: EntityPos, -} diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index 5814687a..c3c37460 100755 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -1,12 +1,13 @@ //! Significantly abstract azalea-protocol so it's actually useable for bots. -mod connect; -mod entity; +mod account; +mod client; +mod movement; pub mod ping; mod player; -pub use connect::{Account, Client, Event}; -pub use entity::Entity; +pub use account::Account; +pub use client::{Client, Event}; pub use player::Player; #[cfg(test)] diff --git a/azalea-client/src/movement.rs b/azalea-client/src/movement.rs new file mode 100644 index 00000000..f74d48df --- /dev/null +++ b/azalea-client/src/movement.rs @@ -0,0 +1,45 @@ +use crate::Client; +use azalea_core::EntityPos; +use azalea_protocol::packets::game::serverbound_move_player_packet_pos_rot::ServerboundMovePlayerPacketPosRot; + +impl Client { + /// Set the client's position to the given coordinates. + pub async fn move_to(&mut self, new_pos: EntityPos) -> Result<(), String> { + println!("obtaining lock on state"); + let mut state_lock = self.state.lock().unwrap(); + println!("obtained lock on state"); + + let world = state_lock.world.as_ref().unwrap(); + + let player = &state_lock.player; + let player_id = if let Some(player) = player.entity(world) { + player.id + } else { + return Err("Player entity not found".to_string()); + }; + + let world = state_lock.world.as_mut().unwrap(); + world.move_entity(player_id, new_pos)?; + drop(state_lock); + + println!("obtaining lock on conn"); + self.conn + .lock() + .await + .write( + ServerboundMovePlayerPacketPosRot { + x: new_pos.x, + y: new_pos.y, + z: new_pos.z, + x_rot: 0.0, + y_rot: 0.0, + on_ground: false, + } + .get(), + ) + .await; + println!("obtained lock on conn"); + + Ok(()) + } +} diff --git a/azalea-client/src/player.rs b/azalea-client/src/player.rs index 04d34f6d..ee0b9718 100644 --- a/azalea-client/src/player.rs +++ b/azalea-client/src/player.rs @@ -1,7 +1,27 @@ -use crate::Entity; +use azalea_entity::Entity; +use azalea_world::World; +use uuid::Uuid; #[derive(Default, Debug)] pub struct Player { - /// The entity attached to the player. There's some useful fields here. - pub entity: Entity, + /// The player's uuid. + pub uuid: Uuid, + /// The player's entity id. + pub entity_id: u32, +} + +impl Player { + /// Get the entity of the player in the world. + pub fn entity<'a>(&self, world: &'a World) -> Option<&'a Entity> { + // world.entity_by_uuid(&self.uuid) + world.entity_by_id(self.entity_id) + } + + pub fn set_uuid(&mut self, uuid: Uuid) { + self.uuid = uuid; + } + + pub fn set_entity_id(&mut self, entity_id: u32) { + self.entity_id = entity_id; + } } |
