use num_derive::{FromPrimitive, ToPrimitive}; use crate::chat::Message; use crate::impl_enum_encoder_decoder; use crate::DecodeError; use crate::Decoder; use minecraft_protocol_derive::Packet; use nbt::CompoundTag; use std::io::Read; pub enum GameServerBoundPacket { ServerBoundChatMessage(ServerBoundChatMessage), ServerBoundKeepAlive(ServerBoundKeepAlive), } pub enum GameClientBoundPacket { ClientBoundChatMessage(ClientBoundChatMessage), JoinGame(JoinGame), ClientBoundKeepAlive(ClientBoundKeepAlive), ChunkData(ChunkData), } impl GameServerBoundPacket { pub fn get_type_id(&self) -> u8 { match self { GameServerBoundPacket::ServerBoundChatMessage(_) => 0x03, GameServerBoundPacket::ServerBoundKeepAlive(_) => 0x0F, } } pub fn decode(type_id: u8, reader: &mut R) -> Result { match type_id { 0x03 => { let chat_message = ServerBoundChatMessage::decode(reader)?; Ok(GameServerBoundPacket::ServerBoundChatMessage(chat_message)) } 0x0F => { let keep_alive = ServerBoundKeepAlive::decode(reader)?; Ok(GameServerBoundPacket::ServerBoundKeepAlive(keep_alive)) } _ => Err(DecodeError::UnknownPacketType { type_id }), } } } impl GameClientBoundPacket { pub fn get_type_id(&self) -> u8 { match self { GameClientBoundPacket::ClientBoundChatMessage(_) => 0x0E, GameClientBoundPacket::ClientBoundKeepAlive(_) => 0x20, GameClientBoundPacket::ChunkData(_) => 0x21, GameClientBoundPacket::JoinGame(_) => 0x25, } } pub fn decode(type_id: u8, reader: &mut R) -> Result { match type_id { 0x0E => { let chat_message = ClientBoundChatMessage::decode(reader)?; Ok(GameClientBoundPacket::ClientBoundChatMessage(chat_message)) } 0x20 => { let keep_alive = ClientBoundKeepAlive::decode(reader)?; Ok(GameClientBoundPacket::ClientBoundKeepAlive(keep_alive)) } 0x21 => { let chunk_data = ChunkData::decode(reader)?; Ok(GameClientBoundPacket::ChunkData(chunk_data)) } 0x25 => { let join_game = JoinGame::decode(reader)?; Ok(GameClientBoundPacket::JoinGame(join_game)) } _ => Err(DecodeError::UnknownPacketType { type_id }), } } } #[derive(Packet, Debug)] pub struct ServerBoundChatMessage { #[packet(max_length = 256)] pub message: String, } impl ServerBoundChatMessage { pub fn new(message: String) -> GameServerBoundPacket { let chat_message = ServerBoundChatMessage { message }; GameServerBoundPacket::ServerBoundChatMessage(chat_message) } } #[derive(Packet, Debug)] pub struct ClientBoundChatMessage { pub message: Message, pub position: MessagePosition, } #[derive(Debug, Eq, PartialEq, FromPrimitive, ToPrimitive)] pub enum MessagePosition { Chat, System, HotBar, } impl_enum_encoder_decoder!(MessagePosition); impl ClientBoundChatMessage { pub fn new(message: Message, position: MessagePosition) -> GameClientBoundPacket { let chat_message = ClientBoundChatMessage { message, position }; GameClientBoundPacket::ClientBoundChatMessage(chat_message) } } #[derive(Packet, Debug)] pub struct JoinGame { pub entity_id: u32, pub game_mode: GameMode, pub dimension: i32, pub max_players: u8, #[packet(max_length = 16)] pub level_type: String, #[packet(with = "var_int")] pub view_distance: i32, pub reduced_debug_info: bool, } #[derive(Debug, Eq, PartialEq, FromPrimitive, ToPrimitive)] pub enum GameMode { Survival = 0, Creative = 1, Adventure = 2, Spectator = 3, Hardcore = 8, } impl_enum_encoder_decoder!(GameMode); impl JoinGame { pub fn new( entity_id: u32, game_mode: GameMode, dimension: i32, max_players: u8, level_type: String, view_distance: i32, reduced_debug_info: bool, ) -> GameClientBoundPacket { let join_game = JoinGame { entity_id, game_mode, dimension, max_players, level_type, view_distance, reduced_debug_info, }; GameClientBoundPacket::JoinGame(join_game) } } #[derive(Packet)] pub struct ServerBoundKeepAlive { pub id: u64, } impl ServerBoundKeepAlive { pub fn new(id: u64) -> GameServerBoundPacket { let keep_alive = ServerBoundKeepAlive { id }; GameServerBoundPacket::ServerBoundKeepAlive(keep_alive) } } #[derive(Packet)] pub struct ClientBoundKeepAlive { pub id: u64, } impl ClientBoundKeepAlive { pub fn new(id: u64) -> GameClientBoundPacket { let keep_alive = ClientBoundKeepAlive { id }; GameClientBoundPacket::ClientBoundKeepAlive(keep_alive) } } #[derive(Packet, Debug)] pub struct ChunkData { pub x: i32, pub z: i32, pub full: bool, #[packet(with = "var_int")] pub primary_mask: i32, pub heights: CompoundTag, pub data: Vec, pub tiles: Vec, } impl ChunkData { pub fn new( x: i32, z: i32, full: bool, primary_mask: i32, heights: CompoundTag, data: Vec, tiles: Vec, ) -> GameClientBoundPacket { let chunk_data = ChunkData { x, z, full, primary_mask, heights, data, tiles, }; GameClientBoundPacket::ChunkData(chunk_data) } } #[cfg(test)] mod tests { use crate::chat::{Message, Payload}; use crate::game::{ ChunkData, ClientBoundChatMessage, ClientBoundKeepAlive, GameMode, JoinGame, MessagePosition, ServerBoundChatMessage, ServerBoundKeepAlive, }; use crate::{DecodeError, Encoder, EncoderWriteExt, STRING_MAX_LENGTH}; use crate::{Decoder, EncodeError}; use nbt::CompoundTag; use std::io::Cursor; #[test] fn test_server_bound_chat_message_encode() { let chat_message = ServerBoundChatMessage { message: String::from("hello server!"), }; let mut vec = Vec::new(); chat_message.encode(&mut vec).unwrap(); assert_eq!( vec, include_bytes!("../test/packet/game/server_bound_chat_message.dat").to_vec() ); } #[test] fn test_server_bound_chat_message_decode() { let mut cursor = Cursor::new( include_bytes!("../test/packet/game/server_bound_chat_message.dat").to_vec(), ); let chat_message = ServerBoundChatMessage::decode(&mut cursor).unwrap(); assert_eq!(chat_message.message, "hello server!"); } #[test] fn test_server_bound_chat_message_encode_invalid_length() { let chat_message = ServerBoundChatMessage { message: "abc".repeat(100), }; let mut vec = Vec::new(); let encode_error = chat_message .encode(&mut vec) .err() .expect("Expected error `StringTooLong` because message has invalid length"); match encode_error { EncodeError::StringTooLong { length, max_length } => { assert_eq!(length, 300); assert_eq!(max_length, 256); } _ => panic!("Expected `StringTooLong` but got `{:?}`", encode_error), } } #[test] fn test_server_bound_chat_message_decode_invalid_length() { let message = "abc".repeat(100); let mut vec = Vec::new(); vec.write_string(&message, STRING_MAX_LENGTH).unwrap(); let mut cursor = Cursor::new(vec); let decode_error = ServerBoundChatMessage::decode(&mut cursor) .err() .expect("Expected error `StringTooLong` because message has invalid length"); match decode_error { DecodeError::StringTooLong { length, max_length } => { assert_eq!(length, 300); assert_eq!(max_length, 256); } _ => panic!("Expected `StringTooLong` but got `{:?}`", decode_error), } } #[test] fn test_client_bound_chat_message_encode() { let chat_message = ClientBoundChatMessage { message: Message::new(Payload::text("hello client!")), position: MessagePosition::System, }; let mut vec = Vec::new(); chat_message.encode(&mut vec).unwrap(); assert_eq!( vec, include_bytes!("../test/packet/game/client_bound_chat_message.dat").to_vec() ); } #[test] fn test_client_bound_chat_message_decode() { let mut cursor = Cursor::new( include_bytes!("../test/packet/game/client_bound_chat_message.dat").to_vec(), ); let chat_message = ClientBoundChatMessage::decode(&mut cursor).unwrap(); assert_eq!( chat_message.message, Message::new(Payload::text("hello client!")) ); assert_eq!(chat_message.position, MessagePosition::System); } #[test] fn test_server_bound_keep_alive_encode() { let keep_alive = ServerBoundKeepAlive { id: 31122019 }; let mut vec = Vec::new(); keep_alive.encode(&mut vec).unwrap(); assert_eq!( vec, include_bytes!("../test/packet/game/server_bound_keep_alive.dat").to_vec() ); } #[test] fn test_server_bound_keep_alive_decode() { let mut cursor = Cursor::new(include_bytes!("../test/packet/game/server_bound_keep_alive.dat").to_vec()); let keep_alive = ServerBoundKeepAlive::decode(&mut cursor).unwrap(); assert_eq!(keep_alive.id, 31122019); } #[test] fn test_client_bound_keep_alive_encode() { let keep_alive = ClientBoundKeepAlive { id: 240714 }; let mut vec = Vec::new(); keep_alive.encode(&mut vec).unwrap(); assert_eq!( vec, include_bytes!("../test/packet/game/client_bound_keep_alive.dat").to_vec() ); } #[test] fn test_client_bound_keep_alive_decode() { let mut cursor = Cursor::new(include_bytes!("../test/packet/game/client_bound_keep_alive.dat").to_vec()); let keep_alive = ClientBoundKeepAlive::decode(&mut cursor).unwrap(); assert_eq!(keep_alive.id, 240714); } #[test] fn test_join_game_encode() { let join_game = JoinGame { entity_id: 27, game_mode: GameMode::Spectator, dimension: 23, max_players: 100, level_type: String::from("default"), view_distance: 10, reduced_debug_info: true, }; let mut vec = Vec::new(); join_game.encode(&mut vec).unwrap(); assert_eq!( vec, include_bytes!("../test/packet/game/join_game.dat").to_vec() ); } #[test] fn test_join_game_decode() { let mut cursor = Cursor::new(include_bytes!("../test/packet/game/join_game.dat").to_vec()); let join_game = JoinGame::decode(&mut cursor).unwrap(); assert_eq!(join_game.entity_id, 27); assert_eq!(join_game.game_mode, GameMode::Spectator); assert_eq!(join_game.dimension, 23); assert_eq!(join_game.max_players, 100); assert_eq!(join_game.level_type, String::from("default")); assert_eq!(join_game.view_distance, 10); assert!(join_game.reduced_debug_info); } #[test] fn test_chunk_data_encode() { let chunk_data = ChunkData { x: -2, z: 5, full: true, primary_mask: 65535, heights: CompoundTag::named("HeightMaps"), data: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10], tiles: vec![CompoundTag::named("TileEntity")], }; let mut vec = Vec::new(); chunk_data.encode(&mut vec).unwrap(); assert_eq!( vec, include_bytes!("../test/packet/game/chunk_data.dat").to_vec() ); } #[test] fn test_chunk_data_decode() { let mut cursor = Cursor::new(include_bytes!("../test/packet/game/chunk_data.dat").to_vec()); let chunk_data = ChunkData::decode(&mut cursor).unwrap(); assert_eq!(chunk_data.x, -2); assert_eq!(chunk_data.z, 5); assert!(chunk_data.full); assert_eq!(chunk_data.heights.name, Some(String::from("HeightMaps"))); assert_eq!(chunk_data.data, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); assert_eq!(chunk_data.primary_mask, 65535); assert_eq!(chunk_data.tiles[0].name, Some(String::from("TileEntity"))); } }