diff --git a/azalea-client/src/connect.rs b/azalea-client/src/connect.rs index 6ed25bff..7b1da525 100644 --- a/azalea-client/src/connect.rs +++ b/azalea-client/src/connect.rs @@ -70,6 +70,9 @@ pub async fn join_server(address: &ServerAddress) -> Result<(), String> { GamePacket::ClientboundCustomPayloadPacket(p) => { println!("Got custom payload packet {:?}", p); } + GamePacket::ClientboundChangeDifficultyPacket(p) => { + println!("Got difficulty packet {:?}", p); + } }, Err(e) => { println!("Error: {:?}", e); diff --git a/azalea-core/src/difficulty.rs b/azalea-core/src/difficulty.rs new file mode 100644 index 00000000..21e980ba --- /dev/null +++ b/azalea-core/src/difficulty.rs @@ -0,0 +1,96 @@ +use std::fmt::{Debug, Error, Formatter}; + +#[derive(Hash, Clone, Debug, PartialEq)] +pub enum Difficulty { + PEACEFUL, + EASY, + NORMAL, + HARD, +} + +pub enum Err { + InvalidDifficulty(String), +} + +impl Debug for Err { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + match self { + Err::InvalidDifficulty(s) => write!(f, "Invalid difficulty: {}", s), + } + } +} + +impl Difficulty { + pub fn name(&self) -> &'static str { + match self { + Difficulty::PEACEFUL => "peaceful", + Difficulty::EASY => "easy", + Difficulty::NORMAL => "normal", + Difficulty::HARD => "hard", + } + } + + pub fn from_name(name: &str) -> Result { + match name { + "peaceful" => Ok(Difficulty::PEACEFUL), + "easy" => Ok(Difficulty::EASY), + "normal" => Ok(Difficulty::NORMAL), + "hard" => Ok(Difficulty::HARD), + _ => Err(Err::InvalidDifficulty(name.to_string())), + } + } + + pub fn by_id(id: u8) -> Difficulty { + match id % 4 { + 0 => Difficulty::PEACEFUL, + 1 => Difficulty::EASY, + 2 => Difficulty::NORMAL, + 3 => Difficulty::HARD, + // this shouldn't be possible because of the modulo, so panicking is fine + _ => panic!("Unknown difficulty id: {}", id), + } + } + + pub fn id(&self) -> u8 { + match self { + Difficulty::PEACEFUL => 0, + Difficulty::EASY => 1, + Difficulty::NORMAL => 2, + Difficulty::HARD => 3, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_difficulty_from_name() { + assert_eq!( + Difficulty::PEACEFUL, + Difficulty::from_name("peaceful").unwrap() + ); + assert_eq!(Difficulty::EASY, Difficulty::from_name("easy").unwrap()); + assert_eq!(Difficulty::NORMAL, Difficulty::from_name("normal").unwrap()); + assert_eq!(Difficulty::HARD, Difficulty::from_name("hard").unwrap()); + assert!(Difficulty::from_name("invalid").is_err()); + } + + #[test] + fn test_difficulty_id() { + assert_eq!(0, Difficulty::PEACEFUL.id()); + assert_eq!(1, Difficulty::EASY.id()); + assert_eq!(2, Difficulty::NORMAL.id()); + assert_eq!(3, Difficulty::HARD.id()); + assert_eq!(4, Difficulty::PEACEFUL.id()); + } + + #[test] + fn test_difficulty_name() { + assert_eq!("peaceful", Difficulty::PEACEFUL.name()); + assert_eq!("easy", Difficulty::EASY.name()); + assert_eq!("normal", Difficulty::NORMAL.name()); + assert_eq!("hard", Difficulty::HARD.name()); + } +} diff --git a/azalea-core/src/lib.rs b/azalea-core/src/lib.rs index 887d1686..cdf07c43 100644 --- a/azalea-core/src/lib.rs +++ b/azalea-core/src/lib.rs @@ -1,14 +1,6 @@ //! Random miscellaneous things like UUIDs that don't deserve their own crate. +pub mod difficulty; pub mod game_type; pub mod resource_location; pub mod serializable_uuid; - -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); - } -} diff --git a/azalea-protocol/src/mc_buf/read.rs b/azalea-protocol/src/mc_buf/read.rs index 5127860e..0fa1d099 100644 --- a/azalea-protocol/src/mc_buf/read.rs +++ b/azalea-protocol/src/mc_buf/read.rs @@ -1,5 +1,8 @@ use async_trait::async_trait; -use azalea_core::{game_type::GameType, resource_location::ResourceLocation}; +use azalea_core::{ + difficulty::Difficulty, game_type::GameType, resource_location::ResourceLocation, +}; +use num_traits::FromPrimitive; use tokio::io::{AsyncRead, AsyncReadExt}; use super::MAX_STRING_LENGTH; @@ -338,6 +341,28 @@ impl McBufReadable for bool { } } +// u8 +#[async_trait] +impl McBufReadable for u8 { + async fn read_into(buf: &mut R) -> Result + where + R: AsyncRead + std::marker::Unpin + std::marker::Send, + { + buf.read_byte().await + } +} + +// i8 +#[async_trait] +impl McBufReadable for i8 { + async fn read_into(buf: &mut R) -> Result + where + R: AsyncRead + std::marker::Unpin + std::marker::Send, + { + buf.read_byte().await.map(|i| i as i8) + } +} + // GameType #[async_trait] impl McBufReadable for GameType { @@ -386,3 +411,14 @@ impl McBufReadable for azalea_nbt::Tag { buf.read_nbt().await } } + +// Difficulty +#[async_trait] +impl McBufReadable for Difficulty { + async fn read_into(buf: &mut R) -> Result + where + R: AsyncRead + std::marker::Unpin + std::marker::Send, + { + Ok(Difficulty::by_id(u8::read_into(buf).await?)) + } +} diff --git a/azalea-protocol/src/mc_buf/write.rs b/azalea-protocol/src/mc_buf/write.rs index 6fbe6eab..fd9faeb4 100644 --- a/azalea-protocol/src/mc_buf/write.rs +++ b/azalea-protocol/src/mc_buf/write.rs @@ -1,6 +1,9 @@ use async_trait::async_trait; -use azalea_core::{game_type::GameType, resource_location::ResourceLocation}; +use azalea_core::{ + difficulty::Difficulty, game_type::GameType, resource_location::ResourceLocation, +}; use byteorder::{BigEndian, WriteBytesExt}; +use num_traits::FromPrimitive; use std::io::Write; use super::MAX_STRING_LENGTH; @@ -255,6 +258,13 @@ impl McBufWritable for bool { } } +// i8 +impl McBufWritable for i8 { + fn write_into(&self, buf: &mut Vec) -> Result<(), std::io::Error> { + buf.write_byte(*self as u8) + } +} + // GameType impl McBufWritable for GameType { fn write_into(&self, buf: &mut Vec) -> Result<(), std::io::Error> { @@ -284,3 +294,10 @@ impl McBufWritable for azalea_nbt::Tag { buf.write_nbt(self) } } + +// Difficulty +impl McBufWritable for Difficulty { + fn write_into(&self, buf: &mut Vec) -> Result<(), std::io::Error> { + u8::write_into(&self.id(), buf) + } +} diff --git a/azalea-protocol/src/packets/game/clientbound_change_difficulty_packet.rs b/azalea-protocol/src/packets/game/clientbound_change_difficulty_packet.rs new file mode 100644 index 00000000..e12cfff3 --- /dev/null +++ b/azalea-protocol/src/packets/game/clientbound_change_difficulty_packet.rs @@ -0,0 +1,8 @@ +use azalea_core::difficulty::Difficulty; +use packet_macros::GamePacket; + +#[derive(Clone, Debug, GamePacket)] +pub struct ClientboundChangeDifficultyPacket { + pub difficulty: Difficulty, + pub locked: bool, +} diff --git a/azalea-protocol/src/packets/game/mod.rs b/azalea-protocol/src/packets/game/mod.rs index 43b3ca3d..4efe72fb 100644 --- a/azalea-protocol/src/packets/game/mod.rs +++ b/azalea-protocol/src/packets/game/mod.rs @@ -1,3 +1,4 @@ +pub mod clientbound_change_difficulty_packet; pub mod clientbound_custom_payload_packet; pub mod clientbound_login_packet; pub mod clientbound_update_view_distance_packet; @@ -18,12 +19,16 @@ where ClientboundCustomPayloadPacket( clientbound_custom_payload_packet::ClientboundCustomPayloadPacket, ), + ClientboundChangeDifficultyPacket( + clientbound_change_difficulty_packet::ClientboundChangeDifficultyPacket, + ), } #[async_trait] impl ProtocolPacket for GamePacket { fn id(&self) -> u32 { match self { + GamePacket::ClientboundChangeDifficultyPacket(_packet) => 0x0e, GamePacket::ClientboundCustomPayloadPacket(_packet) => 0x18, GamePacket::ClientboundLoginPacket(_packet) => 0x26, GamePacket::ClientboundUpdateViewDistancePacket(_packet) => 0x4a, @@ -31,7 +36,12 @@ impl ProtocolPacket for GamePacket { } fn write(&self, buf: &mut Vec) -> Result<(), std::io::Error> { - Ok(()) + match self { + GamePacket::ClientboundChangeDifficultyPacket(packet) => packet.write(buf), + GamePacket::ClientboundCustomPayloadPacket(packet) => packet.write(buf), + GamePacket::ClientboundLoginPacket(packet) => packet.write(buf), + GamePacket::ClientboundUpdateViewDistancePacket(packet) => packet.write(buf), + } } /// Read a packet by its id, ConnectionProtocol, and flow @@ -45,6 +55,9 @@ impl ProtocolPacket for GamePacket { { Ok(match flow { PacketFlow::ServerToClient => match id { + 0x0e => clientbound_change_difficulty_packet::ClientboundChangeDifficultyPacket + ::read(buf) + .await?, 0x18 => clientbound_custom_payload_packet::ClientboundCustomPayloadPacket::read(buf).await?, 0x26 => clientbound_login_packet::ClientboundLoginPacket::read(buf).await?, 0x4a => clientbound_update_view_distance_packet::ClientboundUpdateViewDistancePacket