aboutsummaryrefslogtreecommitdiff
path: root/azalea-client/src/client.rs
diff options
context:
space:
mode:
authormat <github@matdoes.dev>2022-06-18 16:54:49 -0500
committermat <github@matdoes.dev>2022-06-18 16:54:49 -0500
commitfc3151f89db1cf018bfebebb8f102e20911e64d3 (patch)
treee04047dbcbfd9d9e8c6b7a98658ccdb4802eeabd /azalea-client/src/client.rs
parente32b8fabb78a86e073c7bb3270b1bc89a532350a (diff)
downloadazalea-drasl-fc3151f89db1cf018bfebebb8f102e20911e64d3.tar.xz
account.rs and client.rs
Diffstat (limited to 'azalea-client/src/client.rs')
-rw-r--r--azalea-client/src/client.rs465
1 files changed, 465 insertions, 0 deletions
diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs
new file mode 100644
index 00000000..ff8729cb
--- /dev/null
+++ b/azalea-client/src/client.rs
@@ -0,0 +1,465 @@
+use crate::{Account, Player};
+use azalea_core::{resource_location::ResourceLocation, ChunkPos};
+use azalea_entity::Entity;
+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, EntityStorage, World};
+use std::{fmt::Debug, sync::Arc};
+use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
+use tokio::sync::Mutex;
+
+#[derive(Default)]
+pub struct ClientState {
+ pub player: Player,
+ pub world: Option<World>,
+}
+
+#[derive(Debug, Clone)]
+pub enum Event {
+ Login,
+ Chat(ChatPacket),
+}
+
+#[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,
+// }
+// }
+// }
+
+/// 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
+}
+
+/// Whether we should ignore errors when decoding packets.
+const IGNORE_ERRORS: bool = !cfg!(debug_assertions);
+
+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 = 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),
+ entities: EntityStorage::new(),
+ });
+
+ 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 entity = Entity::from(p);
+ state
+ .lock()
+ .await
+ .world
+ .as_mut()
+ .expect("World doesn't exist! We should've gotten a login packet by now.")
+ .entities
+ .insert(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);
+ }
+ 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
+ }
+}