aboutsummaryrefslogtreecommitdiff
path: root/azalea-client
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2022-06-25 05:09:26 +0000
committerGitHub <noreply@github.com>2022-06-25 05:09:26 +0000
commit7d3e57763e32ac9cf94180b1c714704cfbc3034d (patch)
tree2dcfe72bf09a42f6614f9dc988dc0254162ea0bf /azalea-client
parent69c47eda4c496b13dadd80976bffd2fab7ea5894 (diff)
parentca7067e173129f3044ebc8c77634f06da29a086e (diff)
downloadazalea-drasl-7d3e57763e32ac9cf94180b1c714704cfbc3034d.tar.xz
Merge pull request #10 from mat-1/azalea-entity
azalea-entity
Diffstat (limited to 'azalea-client')
-rwxr-xr-xazalea-client/Cargo.toml3
-rw-r--r--azalea-client/src/account.rs20
-rw-r--r--azalea-client/src/client.rs673
-rwxr-xr-xazalea-client/src/connect.rs477
-rw-r--r--azalea-client/src/entity.rs8
-rwxr-xr-xazalea-client/src/lib.rs9
-rw-r--r--azalea-client/src/movement.rs45
-rw-r--r--azalea-client/src/player.rs26
8 files changed, 769 insertions, 492 deletions
diff --git a/azalea-client/Cargo.toml b/azalea-client/Cargo.toml
index eaf7b0dc..b39d6a49 100755
--- a/azalea-client/Cargo.toml
+++ b/azalea-client/Cargo.toml
@@ -9,6 +9,9 @@ version = "0.1.0"
azalea-auth = {path = "../azalea-auth"}
azalea-core = {path = "../azalea-core"}
azalea-crypto = {path = "../azalea-crypto"}
+azalea-entity = {path = "../azalea-entity"}
azalea-protocol = {path = "../azalea-protocol"}
azalea-world = {path = "../azalea-world"}
+owning_ref = "0.4.1"
tokio = {version = "1.19.2", features = ["sync"]}
+uuid = "1.1.2"
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;
+ }
}