From ba911a8a207eb47df7a055410570767b2e33c2ae Mon Sep 17 00:00:00 2001 From: mat Date: Sat, 11 Dec 2021 15:17:42 -0600 Subject: [PATCH] correct minecraft-chat :tada: --- Cargo.lock | 16 + bot/src/main.rs | 1 - minecraft-chat/Cargo.toml | 1 + minecraft-chat/src/component.rs | 320 ++++++++++-------- minecraft-chat/src/style.rs | 170 +++++----- minecraft-chat/src/text_component.rs | 93 +++-- minecraft-chat/tests/integration_test.rs | 13 +- minecraft-protocol/Cargo.toml | 7 +- minecraft-protocol/src/connection.rs | 6 +- minecraft-protocol/src/lib.rs | 2 +- minecraft-protocol/src/mc_buf.rs | 8 +- .../clientbound_status_response_packet.rs | 43 ++- minecraft-protocol/src/resolver.rs | 3 +- .../src/server_status_pinger.rs | 14 +- 14 files changed, 395 insertions(+), 302 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f82d7659..3847548f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -289,6 +289,7 @@ name = "minecraft-chat" version = "0.1.0" dependencies = [ "lazy_static", + "serde", "serde_json", ] @@ -308,6 +309,7 @@ dependencies = [ "byteorder", "bytes", "minecraft-chat", + "serde", "serde_json", "thiserror", "tokio", @@ -505,6 +507,20 @@ name = "serde" version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "serde_json" diff --git a/bot/src/main.rs b/bot/src/main.rs index 68fd41bf..47af37b4 100644 --- a/bot/src/main.rs +++ b/bot/src/main.rs @@ -1,4 +1,3 @@ -use minecraft_client; use minecraft_protocol::ServerAddress; use tokio::runtime::Runtime; diff --git a/minecraft-chat/Cargo.toml b/minecraft-chat/Cargo.toml index 81f676af..aa803864 100644 --- a/minecraft-chat/Cargo.toml +++ b/minecraft-chat/Cargo.toml @@ -7,4 +7,5 @@ version = "0.1.0" [dependencies] lazy_static = "1.4.0" +serde = "^1.0.130" serde_json = "^1.0.72" diff --git a/minecraft-chat/src/component.rs b/minecraft-chat/src/component.rs index 300a1228..2ff6111a 100644 --- a/minecraft-chat/src/component.rs +++ b/minecraft-chat/src/component.rs @@ -1,8 +1,11 @@ -use serde_json; +use serde::{ + de::{self, Error}, + Deserialize, Deserializer, +}; use crate::{ base_component::BaseComponent, - style::Style, + style::{ChatFormatting, Style}, text_component::TextComponent, translatable_component::{StringOrComponent, TranslatableComponent}, }; @@ -13,141 +16,15 @@ pub enum Component { Translatable(TranslatableComponent), } +lazy_static! { + pub static ref DEFAULT_STYLE: Style = Style { + color: Some(ChatFormatting::WHITE.try_into().unwrap()), + ..Style::default() + }; +} + /// A chat component impl Component { - pub fn new(json: &serde_json::Value) -> Result { - // we create a component that we might add siblings to - let mut component: Component; - - // if it's primitive, make it a text component - if !json.is_array() && !json.is_object() { - return Ok(Component::Text(TextComponent::new( - json.as_str().unwrap_or("").to_string(), - ))); - } - // if it's an object, do things with { text } and stuff - else if json.is_object() { - if json.get("text").is_some() { - let text = json.get("text").unwrap().as_str().unwrap_or("").to_string(); - component = Component::Text(TextComponent::new(text)); - } else if json.get("translate").is_some() { - let translate = json.get("translate").unwrap().to_string(); - if json.get("with").is_some() { - let with = json.get("with").unwrap().as_array().unwrap(); - let mut with_array = Vec::with_capacity(with.len()); - for i in 0..with.len() { - // if it's a string component with no styling and no siblings, just add a string to with_array - // otherwise add the component to the array - let c = Component::new(&with[i])?; - if let Component::Text(text_component) = c { - if text_component.base.siblings.is_empty() - && text_component.base.style.is_empty() - { - with_array.push(StringOrComponent::String(text_component.text)); - break; - } - } - with_array.push(StringOrComponent::Component(Component::new(&with[i])?)); - } - component = - Component::Translatable(TranslatableComponent::new(translate, with_array)); - } else { - // if it doesn't have a "with", just have the with_array be empty - component = - Component::Translatable(TranslatableComponent::new(translate, Vec::new())); - } - } else if json.get("score").is_some() { - // object = GsonHelper.getAsJsonObject(jsonObject, "score"); - let score_json = json.get("score").unwrap(); - // if (!object.has("name") || !object.has("objective")) throw new JsonParseException("A score component needs a least a name and an objective"); - // ScoreComponent scoreComponent = new ScoreComponent(GsonHelper.getAsString((JsonObject)object, "name"), GsonHelper.getAsString((JsonObject)object, "objective")); - if score_json.get("name").is_none() || score_json.get("objective").is_none() { - return Err( - "A score component needs at least a name and an objective".to_string() - ); - } - // TODO - return Err("score text components aren't yet supported".to_string()); - // component = ScoreComponent - } else if json.get("selector").is_some() { - // } else if (jsonObject.has("selector")) { - // object = this.parseSeparator(type, jsonDeserializationContext, jsonObject); - // SelectorComponent selectorComponent = new SelectorComponent(GsonHelper.getAsString(jsonObject, "selector"), (Optional)object); - - return Err("selector text components aren't yet supported".to_string()); - // } else if (jsonObject.has("keybind")) { - // KeybindComponent keybindComponent = new KeybindComponent(GsonHelper.getAsString(jsonObject, "keybind")); - } else if json.get("keybind").is_some() { - return Err("keybind text components aren't yet supported".to_string()); - } else { - // } else { - // if (!jsonObject.has("nbt")) throw new JsonParseException("Don't know how to turn " + jsonElement + " into a Component"); - if json.get("nbt").is_none() { - return Err(format!("Don't know how to turn {} into a Component", json)); - } - // object = GsonHelper.getAsString(jsonObject, "nbt"); - let _nbt = json.get("nbt").unwrap().to_string(); - // Optional optional = this.parseSeparator(type, jsonDeserializationContext, jsonObject); - let _separator = Component::parse_separator(json)?; - - let _interpret = match json.get("interpret") { - Some(v) => v.as_bool().ok_or(Some(false)).unwrap(), - None => false, - }; - // boolean bl = GsonHelper.getAsBoolean(jsonObject, "interpret", false); - // if (jsonObject.has("block")) { - if json.get("block").is_some() {} - return Err("nbt text components aren't yet supported".to_string()); - // NbtComponent.BlockNbtComponent blockNbtComponent = new NbtComponent.BlockNbtComponent((String)object, bl, GsonHelper.getAsString(jsonObject, "block"), optional); - // } else if (jsonObject.has("entity")) { - // NbtComponent.EntityNbtComponent entityNbtComponent = new NbtComponent.EntityNbtComponent((String)object, bl, GsonHelper.getAsString(jsonObject, "entity"), optional); - // } else { - // if (!jsonObject.has("storage")) throw new JsonParseException("Don't know how to turn " + jsonElement + " into a Component"); - // NbtComponent.StorageNbtComponent storageNbtComponent = new NbtComponent.StorageNbtComponent((String)object, bl, new ResourceLocation(GsonHelper.getAsString(jsonObject, "storage")), optional); - // } - // } - } - // if (jsonObject.has("extra")) { - // object = GsonHelper.getAsJsonArray(jsonObject, "extra"); - // if (object.size() <= 0) throw new JsonParseException("Unexpected empty array of components"); - // for (int i = 0; i < object.size(); ++i) { - // var5_17.append(this.deserialize(object.get(i), type, jsonDeserializationContext)); - // } - // } - // var5_17.setStyle((Style)jsonDeserializationContext.deserialize(jsonElement, Style.class)); - // return var5_17; - // } - if json.get("extra").is_some() { - let extra = match json.get("extra").unwrap().as_array() { - Some(r) => r, - None => return Err("Extra isn't an array".to_string()), - }; - if extra.is_empty() { - return Err("Unexpected empty array of components".to_string()); - } - for extra_component in extra { - component.append(Component::new(extra_component)?); - } - } - - let style = Style::deserialize(json); - component.get_base_mut().style = style; - - return Ok(component); - } - // ok so it's not an object, if it's an array deserialize every item - else if !json.is_array() { - return Err(format!("Don't know how to turn {} into a Component", json)); - } - let json_array = json.as_array().unwrap(); - // the first item in the array is the one that we're gonna return, the others are siblings - let mut component = Component::new(&json_array[0])?; - for i in 1..json_array.len() { - component.append(Component::new(json_array.get(i).unwrap())?); - } - Ok(component) - } - // TODO: is it possible to use a macro so this doesn't have to be duplicated? pub fn get_base_mut(&mut self) -> &mut BaseComponent { @@ -170,15 +47,20 @@ impl Component { } /// Get the "separator" component from the json - fn parse_separator(json: &serde_json::Value) -> Result, String> { + fn parse_separator(json: &serde_json::Value) -> Result, serde_json::Error> { if json.get("separator").is_some() { - return Ok(Some(Component::new(json.get("separator").unwrap())?)); + return Ok(Some(Component::deserialize( + json.get("separator").unwrap(), + )?)); } Ok(None) } /// Convert this component into an ansi string - pub fn to_ansi(&self) -> String { + pub fn to_ansi(&self, default_style: Option<&Style>) -> String { + // default the default_style to white if it's not set + let default_style: &Style = default_style.unwrap_or_else(|| &DEFAULT_STYLE); + // this contains the final string will all the ansi escape codes let mut built_string = String::new(); // this style will update as we visit components @@ -191,7 +73,7 @@ impl Component { }; let component_style = &component.get_base().style; - let ansi_text = running_style.compare_ansi(component_style); + let ansi_text = running_style.compare_ansi(component_style, default_style); built_string.push_str(&ansi_text); built_string.push_str(component_text); @@ -223,3 +105,163 @@ impl IntoIterator for Component { type Item = Component; type IntoIter = std::vec::IntoIter; } + +impl<'de> Deserialize<'de> for Component { + fn deserialize(de: D) -> Result + where + D: Deserializer<'de>, + { + println!("deserializing component"); + let json: serde_json::Value = serde::Deserialize::deserialize(de)?; + println!("made json"); + + // we create a component that we might add siblings to + let mut component: Component; + + // if it's primitive, make it a text component + if !json.is_array() && !json.is_object() { + return Ok(Component::Text(TextComponent::new( + json.as_str().unwrap_or("").to_string(), + ))); + } + // if it's an object, do things with { text } and stuff + else if json.is_object() { + if json.get("text").is_some() { + let text = json.get("text").unwrap().as_str().unwrap_or("").to_string(); + component = Component::Text(TextComponent::new(text)); + } else if json.get("translate").is_some() { + let translate = json.get("translate").unwrap().to_string(); + if json.get("with").is_some() { + let with = json.get("with").unwrap().as_array().unwrap(); + let mut with_array = Vec::with_capacity(with.len()); + for i in 0..with.len() { + // if it's a string component with no styling and no siblings, just add a string to with_array + // otherwise add the component to the array + let c = Component::deserialize(&with[i]).map_err(de::Error::custom)?; + if let Component::Text(text_component) = c { + if text_component.base.siblings.is_empty() + && text_component.base.style.is_empty() + { + with_array.push(StringOrComponent::String(text_component.text)); + break; + } + } + with_array.push(StringOrComponent::Component( + Component::deserialize(&with[i]).map_err(de::Error::custom)?, + )); + } + component = + Component::Translatable(TranslatableComponent::new(translate, with_array)); + } else { + // if it doesn't have a "with", just have the with_array be empty + component = + Component::Translatable(TranslatableComponent::new(translate, Vec::new())); + } + } else if json.get("score").is_some() { + // object = GsonHelper.getAsJsonObject(jsonObject, "score"); + let score_json = json.get("score").unwrap(); + // if (!object.has("name") || !object.has("objective")) throw new JsonParseException("A score component needs a least a name and an objective"); + // ScoreComponent scoreComponent = new ScoreComponent(GsonHelper.getAsString((JsonObject)object, "name"), GsonHelper.getAsString((JsonObject)object, "objective")); + if score_json.get("name").is_none() || score_json.get("objective").is_none() { + return Err(de::Error::missing_field( + "A score component needs at least a name and an objective", + )); + } + // TODO + return Err(de::Error::custom( + "score text components aren't yet supported", + )); + // component = ScoreComponent + } else if json.get("selector").is_some() { + // } else if (jsonObject.has("selector")) { + // object = this.parseSeparator(type, jsonDeserializationContext, jsonObject); + // SelectorComponent selectorComponent = new SelectorComponent(GsonHelper.getAsString(jsonObject, "selector"), (Optional)object); + + return Err(de::Error::custom( + "selector text components aren't yet supported", + )); + // } else if (jsonObject.has("keybind")) { + // KeybindComponent keybindComponent = new KeybindComponent(GsonHelper.getAsString(jsonObject, "keybind")); + } else if json.get("keybind").is_some() { + return Err(de::Error::custom( + "keybind text components aren't yet supported", + )); + } else { + // } else { + // if (!jsonObject.has("nbt")) throw new JsonParseException("Don't know how to turn " + jsonElement + " into a Component"); + if json.get("nbt").is_none() { + return Err(de::Error::custom( + format!("Don't know how to turn {} into a Component", json).as_str(), + )); + } + // object = GsonHelper.getAsString(jsonObject, "nbt"); + let _nbt = json.get("nbt").unwrap().to_string(); + // Optional optional = this.parseSeparator(type, jsonDeserializationContext, jsonObject); + let _separator = Component::parse_separator(&json).map_err(de::Error::custom)?; + + let _interpret = match json.get("interpret") { + Some(v) => v.as_bool().ok_or(Some(false)).unwrap(), + None => false, + }; + // boolean bl = GsonHelper.getAsBoolean(jsonObject, "interpret", false); + // if (jsonObject.has("block")) { + if json.get("block").is_some() {} + return Err(de::Error::custom( + "nbt text components aren't yet supported", + )); + // NbtComponent.BlockNbtComponent blockNbtComponent = new NbtComponent.BlockNbtComponent((String)object, bl, GsonHelper.getAsString(jsonObject, "block"), optional); + // } else if (jsonObject.has("entity")) { + // NbtComponent.EntityNbtComponent entityNbtComponent = new NbtComponent.EntityNbtComponent((String)object, bl, GsonHelper.getAsString(jsonObject, "entity"), optional); + // } else { + // if (!jsonObject.has("storage")) throw new JsonParseException("Don't know how to turn " + jsonElement + " into a Component"); + // NbtComponent.StorageNbtComponent storageNbtComponent = new NbtComponent.StorageNbtComponent((String)object, bl, new ResourceLocation(GsonHelper.getAsString(jsonObject, "storage")), optional); + // } + // } + } + // if (jsonObject.has("extra")) { + // object = GsonHelper.getAsJsonArray(jsonObject, "extra"); + // if (object.size() <= 0) throw new JsonParseException("Unexpected empty array of components"); + // for (int i = 0; i < object.size(); ++i) { + // var5_17.append(this.deserialize(object.get(i), type, jsonDeserializationContext)); + // } + // } + // var5_17.setStyle((Style)jsonDeserializationContext.deserialize(jsonElement, Style.class)); + // return var5_17; + // } + if json.get("extra").is_some() { + let extra = match json.get("extra").unwrap().as_array() { + Some(r) => r, + None => return Err(de::Error::custom("Extra isn't an array")), + }; + if extra.is_empty() { + return Err(de::Error::custom("Unexpected empty array of components")); + } + for extra_component in extra { + let sibling = + Component::deserialize(extra_component).map_err(de::Error::custom)?; + component.append(sibling); + } + } + + let style = Style::deserialize(&json); + component.get_base_mut().style = style; + + return Ok(component); + } + // ok so it's not an object, if it's an array deserialize every item + else if !json.is_array() { + return Err(de::Error::custom( + format!("Don't know how to turn {} into a Component", json).as_str(), + )); + } + let json_array = json.as_array().unwrap(); + // the first item in the array is the one that we're gonna return, the others are siblings + let mut component = Component::deserialize(&json_array[0]).map_err(de::Error::custom)?; + for i in 1..json_array.len() { + component.append( + Component::deserialize(json_array.get(i).unwrap()).map_err(de::Error::custom)?, + ); + } + Ok(component) + } +} diff --git a/minecraft-chat/src/style.rs b/minecraft-chat/src/style.rs index 14fabec5..7b333e5f 100644 --- a/minecraft-chat/src/style.rs +++ b/minecraft-chat/src/style.rs @@ -188,29 +188,21 @@ impl TextColor { } } +// from ChatFormatting to TextColor +impl TryFrom> for TextColor { + type Error = String; + + fn try_from(formatter: ChatFormatting<'_>) -> Result { + if formatter.is_format { + return Err(format!("{} is not a color", formatter.name)); + } + let color = formatter.color.unwrap_or(0); + Ok(Self::new(color, Some(formatter.name.to_string()))) + } +} + #[derive(Clone, Debug)] pub struct Style { - // @Nullable - // final TextColor color; - // @Nullable - // final Boolean bold; - // @Nullable - // final Boolean italic; - // @Nullable - // final Boolean underlined; - // @Nullable - // final Boolean strikethrough; - // @Nullable - // final Boolean obfuscated; - // @Nullable - // final ClickEvent clickEvent; - // @Nullable - // final HoverEvent hoverEvent; - // @Nullable - // final String insertion; - // @Nullable - // final ResourceLocation font; - // these are options instead of just bools because None is different than false in this case pub color: Option, pub bold: Option, @@ -218,17 +210,24 @@ pub struct Style { pub underlined: Option, pub strikethrough: Option, pub obfuscated: Option, + /// Whether it should reset the formatting before applying these styles + pub reset: bool, } impl Style { - pub fn default() -> Style { - Style { + pub fn default() -> Self { + Self::empty() + } + + pub fn empty() -> Self { + Self { color: None, bold: None, italic: None, underlined: None, strikethrough: None, obfuscated: None, + reset: false, } } @@ -251,6 +250,7 @@ impl Style { underlined, strikethrough, obfuscated, + ..Style::default() } } else { Style::default() @@ -268,42 +268,35 @@ impl Style { } /// find the necessary ansi code to get from this style to another - pub fn compare_ansi(&self, after: &Style) -> String { - let should_reset = { + pub fn compare_ansi(&self, after: &Style, default_style: &Style) -> String { + let should_reset = after.reset || // if it used to be bold and now it's not, reset - if self.bold.unwrap_or(false) && !after.bold.unwrap_or(true) { - true - } + (self.bold.unwrap_or(false) && !after.bold.unwrap_or(true)) || // if it used to be italic and now it's not, reset - else if self.italic.unwrap_or(false) && !after.italic.unwrap_or(true) { - true + (self.italic.unwrap_or(false) && !after.italic.unwrap_or(true)) || // if it used to be underlined and now it's not, reset - } else if self.underlined.unwrap_or(false) && !after.underlined.unwrap_or(true) { - true + (self.underlined.unwrap_or(false) && !after.underlined.unwrap_or(true)) || // if it used to be strikethrough and now it's not, reset - } else if self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true) { - true + (self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true)) || // if it used to be obfuscated and now it's not, reset - } else { - self.obfuscated.unwrap_or(false) && !after.obfuscated.unwrap_or(true) - } - }; + (self.obfuscated.unwrap_or(false) && !after.obfuscated.unwrap_or(true)); let mut ansi_codes = String::new(); - let (before, after) = if should_reset { - // if it's true before and none after, make it true after - // if it's false before and none after, make it false after - // we should apply after into before and use that as after - ansi_codes.push_str(Ansi::RESET); - let mut updated_after = self.clone(); - updated_after.apply(after); - (Style::default(), updated_after) - } else { - (self.clone(), after.clone()) - }; + let empty_style = Style::empty(); - println!("should_reset {:?}", should_reset); + let (before, after) = if should_reset { + ansi_codes.push_str(Ansi::RESET); + let mut updated_after = if after.reset { + default_style.clone() + } else { + self.clone() + }; + updated_after.apply(after); + (&empty_style, updated_after) + } else { + (self, after.clone()) + }; // if bold used to be false/default and now it's true, set bold if !before.bold.unwrap_or(false) && after.bold.unwrap_or(false) { @@ -331,7 +324,7 @@ impl Style { if before.color.is_none() && after.color.is_some() { true } else if before.color.is_some() && after.color.is_some() { - before.color.unwrap().value != after.color.as_ref().unwrap().value + before.color.clone().unwrap().value != after.color.as_ref().unwrap().value } else { false } @@ -369,21 +362,14 @@ impl Style { /// Apply a ChatFormatting to this style pub fn apply_formatting(&mut self, formatting: &ChatFormatting) { - match formatting { - &ChatFormatting::BOLD => self.bold = Some(true), - &ChatFormatting::ITALIC => self.italic = Some(true), - &ChatFormatting::UNDERLINE => self.underlined = Some(true), - &ChatFormatting::STRIKETHROUGH => self.strikethrough = Some(true), - &ChatFormatting::OBFUSCATED => self.obfuscated = Some(true), - &ChatFormatting::RESET => { - self.color = None; - self.bold = None; - self.italic = None; - self.underlined = None; - self.strikethrough = None; - self.obfuscated = None; - } - &ChatFormatting { + match *formatting { + ChatFormatting::BOLD => self.bold = Some(true), + ChatFormatting::ITALIC => self.italic = Some(true), + ChatFormatting::UNDERLINE => self.underlined = Some(true), + ChatFormatting::STRIKETHROUGH => self.strikethrough = Some(true), + ChatFormatting::OBFUSCATED => self.obfuscated = Some(true), + ChatFormatting::RESET => self.reset = true, + ChatFormatting { name: _, code: _, is_format: _, @@ -401,6 +387,8 @@ impl Style { #[cfg(test)] mod tests { + use crate::component::DEFAULT_STYLE; + use super::*; #[test] @@ -418,22 +406,15 @@ mod tests { #[test] fn ansi_difference_should_reset() { let style_a = Style { - color: None, bold: Some(true), italic: Some(true), - underlined: None, - strikethrough: None, - obfuscated: None, + ..Style::default() }; let style_b = Style { - color: None, bold: Some(false), - italic: None, - underlined: None, - strikethrough: None, - obfuscated: None, + ..Style::default() }; - let ansi_difference = style_a.compare_ansi(&style_b); + let ansi_difference = style_a.compare_ansi(&style_b, &Style::default()); assert_eq!( ansi_difference, format!( @@ -446,25 +427,40 @@ mod tests { #[test] fn ansi_difference_shouldnt_reset() { let style_a = Style { - color: None, bold: Some(true), - italic: None, - underlined: None, - strikethrough: None, - obfuscated: None, + ..Style::default() }; let style_b = Style { - color: None, - bold: None, italic: Some(true), - underlined: None, - strikethrough: None, - obfuscated: None, + ..Style::default() }; - let ansi_difference = style_a.compare_ansi(&style_b); + let ansi_difference = style_a.compare_ansi(&style_b, &Style::default()); assert_eq!(ansi_difference, Ansi::ITALIC) } + #[test] + fn ansi_difference_explicit_reset() { + let style_a = Style { + bold: Some(true), + ..Style::empty() + }; + let style_b = Style { + italic: Some(true), + reset: true, + ..Style::empty() + }; + let ansi_difference = style_a.compare_ansi(&style_b, &DEFAULT_STYLE); + assert_eq!( + ansi_difference, + format!( + "{reset}{italic}{white}", + reset = Ansi::RESET, + white = Ansi::rgb(ChatFormatting::WHITE.color.unwrap()), + italic = Ansi::ITALIC + ) + ) + } + #[test] fn test_from_code() { assert_eq!( diff --git a/minecraft-chat/src/text_component.rs b/minecraft-chat/src/text_component.rs index 66bde690..a5030fa1 100644 --- a/minecraft-chat/src/text_component.rs +++ b/minecraft-chat/src/text_component.rs @@ -1,4 +1,10 @@ -use crate::{base_component::BaseComponent, component::Component, style::ChatFormatting}; +use std::fmt; + +use crate::{ + base_component::BaseComponent, + component::Component, + style::{ChatFormatting, Style, TextColor}, +}; #[derive(Clone, Debug)] pub struct TextComponent { @@ -10,7 +16,7 @@ const LEGACY_FORMATTING_CODE_SYMBOL: char = '§'; /// Convert a legacy color code string into a Component /// Technically in Minecraft this is done when displaying the text, but AFAIK it's the same as just doing it in TextComponent -pub fn legacy_color_code_to_component(legacy_color_code: &str) -> Component { +pub fn legacy_color_code_to_text_component(legacy_color_code: &str) -> TextComponent { let mut components: Vec = Vec::with_capacity(1); // iterate over legacy_color_code, if it starts with LEGACY_COLOR_CODE_SYMBOL then read the next character and get the style from that // otherwise, add the character to the text @@ -21,24 +27,15 @@ pub fn legacy_color_code_to_component(legacy_color_code: &str) -> Component { if legacy_color_code.chars().nth(i).unwrap() == LEGACY_FORMATTING_CODE_SYMBOL { let formatting_code = legacy_color_code.chars().nth(i + 1).unwrap(); if let Ok(formatter) = ChatFormatting::from_code(formatting_code) { - if components.is_empty() || components.last().unwrap().text.is_empty() { + if components.is_empty() { + components.push(TextComponent::new("".to_string())); + } else if !components.last().unwrap().text.is_empty() { components.push(TextComponent::new("".to_string())); } - println!( - "applying formatter {:?} {:?}", - components.last_mut().unwrap().base.style, - formatter - ); - components - .last_mut() - .unwrap() - .base - .style - .apply_formatting(formatter); - println!( - "applied formatter {:?}", - components.last_mut().unwrap().base.style - ); + + let style = &mut components.last_mut().unwrap().base.style; + // if the formatter is a reset, then we need to reset the style to the default + style.apply_formatting(formatter); } i += 1; } else { @@ -60,36 +57,72 @@ pub fn legacy_color_code_to_component(legacy_color_code: &str) -> Component { final_component.base.siblings.push(component.get()); } - final_component.get() + final_component } impl<'a> TextComponent { pub fn new(text: String) -> Self { - Self { - base: BaseComponent::new(), - text, + // if it contains a LEGACY_FORMATTING_CODE_SYMBOL, format it + if text.contains(LEGACY_FORMATTING_CODE_SYMBOL) { + legacy_color_code_to_text_component(&text) + } else { + Self { + base: BaseComponent::new(), + text, + } } } - pub fn to_string(&self) -> String { - self.text.clone() - } - fn get(self) -> Component { Component::Text(self) } } +impl fmt::Display for TextComponent { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.text.clone()) + } +} + #[cfg(test)] mod tests { + use crate::style::Ansi; + use super::*; #[test] - fn test_legacy_color_code_to_component() { - let component = legacy_color_code_to_component("§lHello §r§1w§2o§3r§4l§5d"); + fn test_hypixel_motd() { + let component = + TextComponent::new("§aHypixel Network §c[1.8-1.18]\n§b§lHAPPY HOLIDAYS".to_string()) + .get(); assert_eq!( - component.to_ansi(), - "\u{1b}[38;2;170;0;170mHello world\u{1b}[m" + component.to_ansi(None), + format!( + "{GREEN}Hypixel Network {RED}[1.8-1.18]\n{BOLD}{AQUA}HAPPY HOLIDAYS{RESET}", + GREEN = Ansi::rgb(ChatFormatting::GREEN.color.unwrap()), + RED = Ansi::rgb(ChatFormatting::RED.color.unwrap()), + AQUA = Ansi::rgb(ChatFormatting::AQUA.color.unwrap()), + BOLD = Ansi::BOLD, + RESET = Ansi::RESET + ) + ); + } + + #[test] + fn test_legacy_color_code_to_component() { + let component = TextComponent::new("§lHello §r§1w§2o§3r§4l§5d".to_string()).get(); + assert_eq!( + component.to_ansi(None), + format!( + "{BOLD}Hello {RESET}{DARK_BLUE}w{DARK_GREEN}o{DARK_AQUA}r{DARK_RED}l{DARK_PURPLE}d{RESET}", + BOLD = Ansi::BOLD, + RESET = Ansi::RESET, + DARK_BLUE = Ansi::rgb(ChatFormatting::DARK_BLUE.color.unwrap()), + DARK_GREEN = Ansi::rgb(ChatFormatting::DARK_GREEN.color.unwrap()), + DARK_AQUA = Ansi::rgb(ChatFormatting::DARK_AQUA.color.unwrap()), + DARK_RED = Ansi::rgb(ChatFormatting::DARK_RED.color.unwrap()), + DARK_PURPLE = Ansi::rgb(ChatFormatting::DARK_PURPLE.color.unwrap()) + ) ); } } diff --git a/minecraft-chat/tests/integration_test.rs b/minecraft-chat/tests/integration_test.rs index 3feff1ed..aac12875 100644 --- a/minecraft-chat/tests/integration_test.rs +++ b/minecraft-chat/tests/integration_test.rs @@ -2,6 +2,7 @@ use minecraft_chat::{ component::Component, style::{Ansi, ChatFormatting, TextColor}, }; +use serde::Deserialize; use serde_json::Value; #[test] @@ -14,9 +15,9 @@ fn basic_ansi_test() { }"#, ) .unwrap(); - let component = Component::new(&j).unwrap(); + let component = Component::deserialize(&j).unwrap(); assert_eq!( - component.to_ansi(), + component.to_ansi(None), "\u{1b}[1m\u{1b}[38;2;255;85;85mhello\u{1b}[m" ); } @@ -50,9 +51,9 @@ fn complex_ansi_test() { ]"##, ) .unwrap(); - let component = Component::new(&j).unwrap(); + let component = Component::deserialize(&j).unwrap(); assert_eq!( - component.to_ansi(), + component.to_ansi(None), format!( "{bold}{italic}{underlined}{red}hello{reset}{bold}{italic}{red} {reset}{italic}{strikethrough}{abcdef}world{reset}{abcdef} asdf{bold}!{reset}", bold = Ansi::BOLD, @@ -69,6 +70,6 @@ fn complex_ansi_test() { #[test] fn component_from_string() { let j: Value = serde_json::from_str("\"foo\"").unwrap(); - let component = Component::new(&j).unwrap(); - assert_eq!(component.to_ansi(), "foo"); + let component = Component::deserialize(&j).unwrap(); + assert_eq!(component.to_ansi(None), "foo"); } diff --git a/minecraft-protocol/Cargo.toml b/minecraft-protocol/Cargo.toml index 7894ecfd..3cbf663b 100644 --- a/minecraft-protocol/Cargo.toml +++ b/minecraft-protocol/Cargo.toml @@ -7,12 +7,13 @@ version = "0.1.0" [dependencies] async-recursion = "^0.3.2" +async-trait = "0.1.51" byteorder = "^1.4.3" bytes = "^1.1.0" +minecraft-chat = {path = "../minecraft-chat"} +serde = {version = "1.0.130", features = ["serde_derive"]} +serde_json = "^1.0.72" thiserror = "^1.0.30" tokio = {version = "^1.14.0", features = ["io-util", "net", "macros"]} tokio-util = "^0.6.9" trust-dns-resolver = "^0.20.3" -async-trait = "0.1.51" -minecraft-chat = { path = "../minecraft-chat" } -serde_json = "^1.0.72" diff --git a/minecraft-protocol/src/connection.rs b/minecraft-protocol/src/connection.rs index cfca403c..2fe03dfb 100644 --- a/minecraft-protocol/src/connection.rs +++ b/minecraft-protocol/src/connection.rs @@ -45,7 +45,7 @@ impl Connection { self.state = state; } - pub async fn read_packet(&mut self) -> Result<(), String> { + pub async fn read_packet(&mut self) -> Result { // what this does: // 1. reads the first 5 bytes, probably only some of this will be used to get the packet length // 2. how much we should read = packet length - 5 @@ -69,9 +69,7 @@ impl Connection { ) .await?; - println!("packet: {:?}", packet); - - Ok(()) + Ok(packet) } /// Write a packet to the server diff --git a/minecraft-protocol/src/lib.rs b/minecraft-protocol/src/lib.rs index 88b3603f..aaf3da50 100644 --- a/minecraft-protocol/src/lib.rs +++ b/minecraft-protocol/src/lib.rs @@ -23,7 +23,7 @@ pub struct ServerIpAddress { impl ServerAddress { /// Convert a Minecraft server address (host:port, the port is optional) to a ServerAddress - pub fn parse(string: &String) -> Result { + pub fn parse(string: &str) -> Result { if string.is_empty() { return Err("Empty string".to_string()); } diff --git a/minecraft-protocol/src/mc_buf.rs b/minecraft-protocol/src/mc_buf.rs index a9ad1642..6c812058 100644 --- a/minecraft-protocol/src/mc_buf.rs +++ b/minecraft-protocol/src/mc_buf.rs @@ -49,7 +49,7 @@ pub async fn read_varint( pub fn write_varint(buf: &mut Vec, mut value: i32) { let mut buffer = [0]; if value == 0 { - buf.write(&buffer).unwrap(); + buf.write_all(&buffer).unwrap(); } while value != 0 { buffer[0] = (value & 0b0111_1111) as u8; @@ -57,7 +57,7 @@ pub fn write_varint(buf: &mut Vec, mut value: i32) { if value != 0 { buffer[0] |= 0b1000_0000; } - buf.write(&buffer).unwrap(); + buf.write_all(&buffer).unwrap(); } } @@ -134,7 +134,7 @@ pub async fn read_utf_with_len( Ok(string) } -pub fn write_utf_with_len(buf: &mut Vec, string: &String, len: usize) { +pub fn write_utf_with_len(buf: &mut Vec, string: &str, len: usize) { if string.len() > len { panic!( "String too big (was {} bytes encoded, max {})", @@ -152,7 +152,7 @@ pub async fn read_utf( read_utf_with_len(buf, MAX_STRING_LENGTH.into()).await } -pub fn write_utf(buf: &mut Vec, string: &String) { +pub fn write_utf(buf: &mut Vec, string: &str) { write_utf_with_len(buf, string, MAX_STRING_LENGTH.into()); } 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 20db9fe1..0868a062 100644 --- a/minecraft-protocol/src/packets/status/clientbound_status_response_packet.rs +++ b/minecraft-protocol/src/packets/status/clientbound_status_response_packet.rs @@ -1,5 +1,6 @@ use async_trait::async_trait; use minecraft_chat::component::Component; +use serde::{Deserialize, Deserializer}; use serde_json::Value; use tokio::io::BufReader; @@ -8,29 +9,29 @@ use crate::{ packets::{Packet, PacketTrait}, }; -#[derive(Clone, Debug)] -struct Version { - name: String, - protocol: u32, +#[derive(Clone, Debug, Deserialize)] +pub struct Version { + pub name: String, + pub protocol: u32, } -#[derive(Clone, Debug)] -struct SamplePlayer { - id: String, - name: String, +#[derive(Clone, Debug, Deserialize)] +pub struct SamplePlayer { + pub id: String, + pub name: String, } -#[derive(Clone, Debug)] -struct Players { - max: u32, - online: u32, - sample: Vec, +#[derive(Clone, Debug, Deserialize)] +pub struct Players { + pub max: u32, + pub online: u32, + pub sample: Vec, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize)] pub struct ClientboundStatusResponsePacket { - // version: Version, - description: Component, + pub version: Version, + pub description: Component, } #[async_trait] @@ -47,13 +48,9 @@ impl PacketTrait for ClientboundStatusResponsePacket { let status_string = mc_buf::read_utf(buf).await?; let status_json: Value = serde_json::from_str(status_string.as_str()).expect("Server status isn't valid JSON"); - let description_string: &Value = status_json.get("description").unwrap(); - // this.status = GsonHelper.fromJson(GSON, friendlyByteBuf.readUtf(32767), ServerStatus.class); - Ok(ClientboundStatusResponsePacket { - // version: status_json.get("version"), - description: Component::new(description_string)?, - } - .get()) + Ok(ClientboundStatusResponsePacket::deserialize(status_json) + .map_err(|e| e.to_string())? + .get()) } } diff --git a/minecraft-protocol/src/resolver.rs b/minecraft-protocol/src/resolver.rs index b751e05f..24687a6e 100644 --- a/minecraft-protocol/src/resolver.rs +++ b/minecraft-protocol/src/resolver.rs @@ -29,8 +29,7 @@ pub async fn resolve_address(address: &ServerAddress) -> Result Result<(), String> { conn.send_packet(ServerboundStatusRequestPacket {}.get()) .await; - conn.read_packet().await.unwrap(); + let packet = conn.read_packet().await.unwrap(); + + match packet { + Packet::ClientboundStatusResponsePacket(p) => { + println!("{:?}", p); + println!("{}", p.description.to_ansi(None)); + } + _ => { + println!("unexpected packet {:?}", packet); + } + } Ok(())