diff --git a/Cargo.lock b/Cargo.lock index 0c25e106..08d718a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -288,6 +288,7 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" name = "minecraft-chat" version = "0.1.0" dependencies = [ + "lazy_static", "serde_json", ] diff --git a/minecraft-chat/Cargo.toml b/minecraft-chat/Cargo.toml index 2578a535..81f676af 100644 --- a/minecraft-chat/Cargo.toml +++ b/minecraft-chat/Cargo.toml @@ -6,4 +6,5 @@ version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +lazy_static = "1.4.0" serde_json = "^1.0.72" diff --git a/minecraft-chat/src/lib.rs b/minecraft-chat/src/lib.rs index 2fbad937..b7035e13 100644 --- a/minecraft-chat/src/lib.rs +++ b/minecraft-chat/src/lib.rs @@ -1,4 +1,8 @@ -//! Things for working with Minecraft chat messages, inspired by the Minecraft source code and prismarine-chat. +//! Things for working with Minecraft chat messages. +//! This was inspired by Minecraft and prismarine-chat. + +#[macro_use] +extern crate lazy_static; pub mod base_component; pub mod component; diff --git a/minecraft-chat/src/style.rs b/minecraft-chat/src/style.rs index c9d419a2..5d5cc8a5 100644 --- a/minecraft-chat/src/style.rs +++ b/minecraft-chat/src/style.rs @@ -9,8 +9,26 @@ pub struct TextColor { } impl TextColor { - // hopefully rust/llvm optimizes this so it's just calculated once - fn calculate_legacy_format_to_color() -> HashMap<&'static ChatFormatting<'static>, TextColor> { + pub fn parse(value: String) -> Result { + if value.starts_with("#") { + let n = value.chars().skip(1).collect::(); + let n = u32::from_str_radix(&n, 16).unwrap(); + return Ok(TextColor::from_rgb(n)); + } + let color = NAMED_COLORS.get(&value.to_ascii_uppercase()); + if color.is_some() { + return Ok(color.unwrap().clone()); + } + Err(format!("Invalid color {}", value)) + } + + fn from_rgb(value: u32) -> TextColor { + TextColor { value, name: None } + } +} + +lazy_static! { + static ref LEGACY_FORMAT_TO_COLOR: HashMap<&'static ChatFormatting<'static>, TextColor> = { let mut legacy_format_to_color = HashMap::new(); for formatter in &ChatFormatting::FORMATTERS { if !formatter.is_format && *formatter != ChatFormatting::RESET { @@ -24,34 +42,14 @@ impl TextColor { } } legacy_format_to_color - } - - fn calculate_named_colors() -> HashMap { - let legacy_format_to_color = Self::calculate_legacy_format_to_color(); + }; + static ref NAMED_COLORS: HashMap = { let mut named_colors = HashMap::new(); - for color in legacy_format_to_color.values() { + for color in LEGACY_FORMAT_TO_COLOR.values() { named_colors.insert(color.name.clone().unwrap(), color.clone()); } named_colors - } - - pub fn parse(value: String) -> Result { - if value.starts_with("#") { - let n = value.chars().skip(1).collect::(); - let n = u32::from_str_radix(&n, 16).unwrap(); - return Ok(TextColor::from_rgb(n)); - } - let named_colors = Self::calculate_named_colors(); - let color = named_colors.get(&value.to_ascii_uppercase()); - if color.is_some() { - return Ok(color.unwrap().clone()); - } - Err(format!("Invalid color {}", value)) - } - - fn from_rgb(value: u32) -> TextColor { - TextColor { value, name: None } - } + }; } /// The weird S character Minecraft used to use for chat formatting diff --git a/minecraft-protocol/src/connection.rs b/minecraft-protocol/src/connection.rs index e5784eb6..ba27cdad 100644 --- a/minecraft-protocol/src/connection.rs +++ b/minecraft-protocol/src/connection.rs @@ -35,7 +35,7 @@ impl Connection { .expect("Error enabling tcp_nodelay"); Ok(Connection { - state: ConnectionProtocol::Handshaking, + state: ConnectionProtocol::Handshake, flow: PacketFlow::ClientToServer, stream, }) @@ -80,7 +80,7 @@ impl Connection { // write the packet id let mut id_and_data_buf = vec![]; - mc_buf::write_varint(&mut id_and_data_buf, packet.get_id() as i32); + mc_buf::write_varint(&mut id_and_data_buf, packet.id() as i32); packet.write(&mut id_and_data_buf); // write the packet data diff --git a/minecraft-protocol/src/mc_buf.rs b/minecraft-protocol/src/mc_buf.rs index 087e66a1..b83a9599 100644 --- a/minecraft-protocol/src/mc_buf.rs +++ b/minecraft-protocol/src/mc_buf.rs @@ -146,7 +146,9 @@ pub fn write_utf_with_len(buf: &mut Vec, string: &String, len: usize) { write_bytes(buf, string.as_bytes()); } -pub async fn read_utf(buf: &mut T) -> Result { +pub async fn read_utf( + buf: &mut BufReader, +) -> Result { read_utf_with_len(buf, MAX_STRING_LENGTH.into()).await } diff --git a/minecraft-protocol/src/packets/handshake/client_intention_packet.rs b/minecraft-protocol/src/packets/handshake/client_intention_packet.rs index 7dd358eb..401dbbac 100644 --- a/minecraft-protocol/src/packets/handshake/client_intention_packet.rs +++ b/minecraft-protocol/src/packets/handshake/client_intention_packet.rs @@ -1,5 +1,8 @@ use std::hash::Hash; +use async_trait::async_trait; +use tokio::io::BufReader; + use crate::{ mc_buf, packets::{ConnectionProtocol, Packet, PacketTrait}, @@ -14,9 +17,10 @@ pub struct ClientIntentionPacket<'a> { pub intention: ConnectionProtocol, } +#[async_trait] impl<'a> PacketTrait for ClientIntentionPacket<'a> { fn get(&self) -> Packet { - Packet::ClientIntentionPacket(self) + Packet::ClientIntentionPacket(*self) } fn write(&self, buf: &mut Vec) { @@ -26,5 +30,10 @@ impl<'a> PacketTrait for ClientIntentionPacket<'a> { mc_buf::write_varint(buf, self.intention.clone() as i32); } - fn parse(&self, buf: T) -> () {} + async fn read( + buf: &mut BufReader, + ) -> Result, String> { + Err("ClientIntentionPacket::parse not implemented".to_string()) + // Ok(ClientIntentionPacket {}.get()) + } } diff --git a/minecraft-protocol/src/packets/mod.rs b/minecraft-protocol/src/packets/mod.rs index 5fb34743..76f9128e 100644 --- a/minecraft-protocol/src/packets/mod.rs +++ b/minecraft-protocol/src/packets/mod.rs @@ -1,10 +1,12 @@ -mod game; -mod handshake; -mod login; -mod status; +pub mod game; +pub mod handshake; +pub mod login; +pub mod status; use async_trait::async_trait; -use tokio::io::AsyncRead; +use tokio::io::{AsyncRead, BufReader}; + +use crate::connection::PacketFlow; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ConnectionProtocol { @@ -16,24 +18,89 @@ pub enum ConnectionProtocol { pub enum Packet<'a> { // game + // handshake - ClientIntentionPacket(&'a handshake::client_intention_packet::ClientIntentionPacket<'a>), + ClientIntentionPacket(handshake::client_intention_packet::ClientIntentionPacket<'a>), + // login + // status ServerboundStatusRequestPacket( - &'a status::serverbound_status_request_packet::ServerboundStatusRequestPacket, + status::serverbound_status_request_packet::ServerboundStatusRequestPacket, ), - ClientboundStatusRequestPacket( - &'a status::clientbound_status_response_packet::ClientboundStatusRequestPacket, + ClientboundStatusResponsePacket( + status::clientbound_status_response_packet::ClientboundStatusResponsePacket, ), } +// TODO: do all this with macros so it's less repetitive +impl Packet<'_> { + fn get_inner_packet(&self) -> &dyn PacketTrait { + match self { + Packet::ClientIntentionPacket(packet) => packet, + Packet::ServerboundStatusRequestPacket(packet) => packet, + Packet::ClientboundStatusResponsePacket(packet) => packet, + } + } + + pub fn id(&self) -> u32 { + match self { + Packet::ClientIntentionPacket(packet) => 0x00, + Packet::ServerboundStatusRequestPacket(packet) => 0x00, + Packet::ClientboundStatusResponsePacket(packet) => 0x00, + } + } + + /// Read a packet by its id, ConnectionProtocol, and flow + pub async fn read( + id: u32, + protocol: ConnectionProtocol, + flow: PacketFlow, + buf: &mut BufReader, + ) -> Result, String> { + match protocol { + ConnectionProtocol::Handshake => match id { + 0x00 => Ok( + handshake::client_intention_packet::ClientIntentionPacket::read(buf).await?, + ), + _ => Err(format!("Unknown packet id: {}", id)), + }, + ConnectionProtocol::Game => Err("Game protocol not implemented yet".to_string()), + ConnectionProtocol::Status => match flow { + PacketFlow::ServerToClient => match id { + 0x00 => Ok( + status::clientbound_status_response_packet::ClientboundStatusResponsePacket + ::read(buf) + .await?, + ), + _ => Err(format!("Unknown packet id: {}", id)), + }, + PacketFlow::ClientToServer => match id { + 0x00 => Ok( + status::serverbound_status_request_packet::ServerboundStatusRequestPacket + ::read(buf) + .await?, + ), + _ => Err(format!("Unknown packet id: {}", id)), + }, + }, + ConnectionProtocol::Login => Err("Login protocol not implemented yet".to_string()), + } + } + + pub fn write(&self, buf: &mut Vec) { + self.get_inner_packet().write(buf); + } +} + +#[async_trait] pub trait PacketTrait { /// Return a version of the packet that you can actually use for stuff fn get(&self) -> Packet; fn write(&self, buf: &mut Vec) -> (); - fn parse( - buf: &mut T, - // is using a static lifetime here a good idea? idk - ) -> Result, String>; + async fn read( + buf: &mut BufReader, + ) -> Result, String> + where + Self: Sized; } diff --git a/minecraft-protocol/src/packets/status/clientbound_status_response_packet.rs b/minecraft-protocol/src/packets/status/clientbound_status_response_packet.rs index ada86667..38747cc8 100644 --- a/minecraft-protocol/src/packets/status/clientbound_status_response_packet.rs +++ b/minecraft-protocol/src/packets/status/clientbound_status_response_packet.rs @@ -1,28 +1,31 @@ use async_trait::async_trait; use std::hash::Hash; +use tokio::io::BufReader; use crate::{ mc_buf, packets::{Packet, PacketTrait}, }; -#[derive(Hash)] -pub struct ClientboundStatusRequestPacket { +#[derive(Hash, Clone)] +pub struct ClientboundStatusResponsePacket { status: String, } -impl PacketTrait for ClientboundStatusRequestPacket { +#[async_trait] +impl PacketTrait for ClientboundStatusResponsePacket { fn get(&self) -> Packet { - Packet::ClientboundStatusRequestPacket(self) + Packet::ClientboundStatusResponsePacket(self.clone()) } fn write(&self, _buf: &mut Vec) {} - fn parse( - buf: &mut T, + async fn read( + buf: &mut BufReader, ) -> Result, String> { - let status = mc_buf::read_utf(&mut buf).await?; + let status = mc_buf::read_utf(buf).await?; // this.status = GsonHelper.fromJson(GSON, friendlyByteBuf.readUtf(32767), ServerStatus.class); - Ok(ClientboundStatusRequestPacket { status }.get()) + let packet = ClientboundStatusResponsePacket { status }.get(); + Ok(packet) } } diff --git a/minecraft-protocol/src/packets/status/serverbound_status_request_packet.rs b/minecraft-protocol/src/packets/status/serverbound_status_request_packet.rs index f2e6fe9a..b3c87968 100644 --- a/minecraft-protocol/src/packets/status/serverbound_status_request_packet.rs +++ b/minecraft-protocol/src/packets/status/serverbound_status_request_packet.rs @@ -1,4 +1,6 @@ +use async_trait::async_trait; use std::hash::Hash; +use tokio::io::BufReader; use crate::{ mc_buf, @@ -8,11 +10,16 @@ use crate::{ #[derive(Hash)] pub struct ServerboundStatusRequestPacket {} -// implement "Packet" for "ClientIntentionPacket" +#[async_trait] impl PacketTrait for ServerboundStatusRequestPacket { fn get(&self) -> Packet { - Packet::ServerboundStatusRequestPacket(self) + Packet::ServerboundStatusRequestPacket(*self) } fn write(&self, _buf: &mut Vec) {} - fn parse(&self, buf: T) -> () {} + + async fn read( + buf: &mut BufReader, + ) -> Result, String> { + Err("ServerboundStatusRequestPacket::read not implemented".to_string()) + } } diff --git a/minecraft-protocol/src/server_status_pinger.rs b/minecraft-protocol/src/server_status_pinger.rs index 342c4f44..1e523511 100644 --- a/minecraft-protocol/src/server_status_pinger.rs +++ b/minecraft-protocol/src/server_status_pinger.rs @@ -1,7 +1,10 @@ use crate::{ connection::Connection, - mc_buf, - packets::{ClientIntentionPacket, ConnectionProtocol, ServerboundStatusRequestPacket}, + packets::{ + handshake::client_intention_packet::ClientIntentionPacket, + status::serverbound_status_request_packet::ServerboundStatusRequestPacket, + ConnectionProtocol, PacketTrait, + }, resolver, ServerAddress, }; @@ -14,17 +17,21 @@ pub async fn ping_server(address: &ServerAddress) -> Result<(), String> { println!("writing intention packet {}", address.host); // send the client intention packet and switch to the status state - conn.send_packet(&ClientIntentionPacket { - protocol_version: 757, - hostname: &address.host, - port: address.port, - intention: ConnectionProtocol::Status, - }) + conn.send_packet( + ClientIntentionPacket { + protocol_version: 757, + hostname: &address.host, + port: address.port, + intention: ConnectionProtocol::Status, + } + .get(), + ) .await; conn.switch_state(ConnectionProtocol::Status); // send the empty status request packet - conn.send_packet(&ServerboundStatusRequestPacket {}).await; + conn.send_packet(ServerboundStatusRequestPacket {}.get()) + .await; conn.read_packet().await.unwrap();